mirror of
https://github.com/esphome/esphome.git
synced 2025-10-12 14:53:49 +01:00
Merge branch 'dev' into mdns_value_flash
This commit is contained in:
@@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
|
|||||||
esphome/components/ens160_i2c/* @latonita
|
esphome/components/ens160_i2c/* @latonita
|
||||||
esphome/components/ens160_spi/* @latonita
|
esphome/components/ens160_spi/* @latonita
|
||||||
esphome/components/ens210/* @itn3rd77
|
esphome/components/ens210/* @itn3rd77
|
||||||
|
esphome/components/epaper_spi/* @esphome/core
|
||||||
esphome/components/es7210/* @kahrendt
|
esphome/components/es7210/* @kahrendt
|
||||||
esphome/components/es7243e/* @kbx81
|
esphome/components/es7243e/* @kbx81
|
||||||
esphome/components/es8156/* @kbx81
|
esphome/components/es8156/* @kbx81
|
||||||
@@ -429,6 +430,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
|||||||
esphome/components/spi/* @clydebarrow @esphome/core
|
esphome/components/spi/* @clydebarrow @esphome/core
|
||||||
esphome/components/spi_device/* @clydebarrow
|
esphome/components/spi_device/* @clydebarrow
|
||||||
esphome/components/spi_led_strip/* @clydebarrow
|
esphome/components/spi_led_strip/* @clydebarrow
|
||||||
|
esphome/components/split_buffer/* @jesserockz
|
||||||
esphome/components/sprinkler/* @kbx81
|
esphome/components/sprinkler/* @kbx81
|
||||||
esphome/components/sps30/* @martgras
|
esphome/components/sps30/* @martgras
|
||||||
esphome/components/ssd1322_base/* @kbx81
|
esphome/components/ssd1322_base/* @kbx81
|
||||||
|
@@ -9,6 +9,7 @@ import esphome.config_validation as cv
|
|||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ACTION,
|
CONF_ACTION,
|
||||||
CONF_ACTIONS,
|
CONF_ACTIONS,
|
||||||
|
CONF_CAPTURE_RESPONSE,
|
||||||
CONF_DATA,
|
CONF_DATA,
|
||||||
CONF_DATA_TEMPLATE,
|
CONF_DATA_TEMPLATE,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
@@ -17,30 +18,50 @@ from esphome.const import (
|
|||||||
CONF_MAX_CONNECTIONS,
|
CONF_MAX_CONNECTIONS,
|
||||||
CONF_ON_CLIENT_CONNECTED,
|
CONF_ON_CLIENT_CONNECTED,
|
||||||
CONF_ON_CLIENT_DISCONNECTED,
|
CONF_ON_CLIENT_DISCONNECTED,
|
||||||
|
CONF_ON_ERROR,
|
||||||
|
CONF_ON_SUCCESS,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_REBOOT_TIMEOUT,
|
CONF_REBOOT_TIMEOUT,
|
||||||
|
CONF_RESPONSE_TEMPLATE,
|
||||||
CONF_SERVICE,
|
CONF_SERVICE,
|
||||||
CONF_SERVICES,
|
CONF_SERVICES,
|
||||||
CONF_TAG,
|
CONF_TAG,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_VARIABLES,
|
CONF_VARIABLES,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||||
|
from esphome.cpp_generator import TemplateArgsType
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "api"
|
DOMAIN = "api"
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
AUTO_LOAD = ["socket"]
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
|
|
||||||
|
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||||
|
"""Conditionally auto-load json only when capture_response is used."""
|
||||||
|
base = ["socket"]
|
||||||
|
|
||||||
|
# Check if any homeassistant.action/homeassistant.service has capture_response: true
|
||||||
|
# This flag is set during config validation in _validate_response_config
|
||||||
|
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
|
||||||
|
return base + ["json"]
|
||||||
|
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
api_ns = cg.esphome_ns.namespace("api")
|
api_ns = cg.esphome_ns.namespace("api")
|
||||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||||
HomeAssistantServiceCallAction = api_ns.class_(
|
HomeAssistantServiceCallAction = api_ns.class_(
|
||||||
"HomeAssistantServiceCallAction", automation.Action
|
"HomeAssistantServiceCallAction", automation.Action
|
||||||
)
|
)
|
||||||
|
ActionResponse = api_ns.class_("ActionResponse")
|
||||||
|
HomeAssistantActionResponseTrigger = api_ns.class_(
|
||||||
|
"HomeAssistantActionResponseTrigger", automation.Trigger
|
||||||
|
)
|
||||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
||||||
|
|
||||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
||||||
@@ -288,6 +309,29 @@ async def to_code(config):
|
|||||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_response_config(config: ConfigType) -> ConfigType:
|
||||||
|
# Validate dependencies:
|
||||||
|
# - response_template requires capture_response: true
|
||||||
|
# - capture_response: true requires on_success
|
||||||
|
if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
|
||||||
|
path=[CONF_RESPONSE_TEMPLATE],
|
||||||
|
)
|
||||||
|
|
||||||
|
if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
|
||||||
|
path=[CONF_CAPTURE_RESPONSE],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track if any action uses capture_response for AUTO_LOAD
|
||||||
|
if config[CONF_CAPTURE_RESPONSE]:
|
||||||
|
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -303,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
||||||
{cv.string: cv.returning_lambda}
|
{cv.string: cv.returning_lambda}
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
|
||||||
|
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
|
||||||
|
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
||||||
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
||||||
|
_validate_response_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -320,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
|||||||
HomeAssistantServiceCallAction,
|
HomeAssistantServiceCallAction,
|
||||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||||
)
|
)
|
||||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
async def homeassistant_service_to_code(
|
||||||
|
config: ConfigType,
|
||||||
|
action_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
):
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||||
@@ -335,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
|||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(key, templ))
|
||||||
|
|
||||||
|
if on_error := config.get(CONF_ON_ERROR):
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
|
||||||
|
cg.add(var.set_wants_status())
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_error_trigger(),
|
||||||
|
[(cg.std_string, "error"), *args],
|
||||||
|
on_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
if on_success := config.get(CONF_ON_SUCCESS):
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||||
|
cg.add(var.set_wants_status())
|
||||||
|
if config[CONF_CAPTURE_RESPONSE]:
|
||||||
|
cg.add(var.set_wants_response())
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_success_trigger_with_response(),
|
||||||
|
[(cg.JsonObjectConst, "response"), *args],
|
||||||
|
on_success,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
|
||||||
|
templ = await cg.templatable(response_template, args, cg.std_string)
|
||||||
|
cg.add(var.set_response_template(templ))
|
||||||
|
|
||||||
|
else:
|
||||||
|
await automation.build_automation(
|
||||||
|
var.get_success_trigger(),
|
||||||
|
args,
|
||||||
|
on_success,
|
||||||
|
)
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@@ -780,6 +780,22 @@ message HomeassistantActionRequest {
|
|||||||
repeated HomeassistantServiceMap data_template = 3;
|
repeated HomeassistantServiceMap data_template = 3;
|
||||||
repeated HomeassistantServiceMap variables = 4;
|
repeated HomeassistantServiceMap variables = 4;
|
||||||
bool is_event = 5;
|
bool is_event = 5;
|
||||||
|
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
||||||
|
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||||
|
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message sent by Home Assistant to ESPHome with service call response data
|
||||||
|
message HomeassistantActionResponse {
|
||||||
|
option (id) = 130;
|
||||||
|
option (source) = SOURCE_CLIENT;
|
||||||
|
option (no_delay) = true;
|
||||||
|
option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
|
||||||
|
|
||||||
|
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
|
||||||
|
bool success = 2; // Whether the service call succeeded
|
||||||
|
string error_message = 3; // Error message if success = false
|
||||||
|
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
||||||
|
@@ -8,9 +8,9 @@
|
|||||||
#endif
|
#endif
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <utility>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
#include "esphome/components/network/util.h"
|
#include "esphome/components/network/util.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
@@ -1549,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
if (msg.response_data_len > 0) {
|
||||||
|
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
|
||||||
|
msg.response_data_len);
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
||||||
NoiseEncryptionSetKeyResponse resp;
|
NoiseEncryptionSetKeyResponse resp;
|
||||||
|
@@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection {
|
|||||||
return;
|
return;
|
||||||
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
#endif
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
|
@@ -884,6 +884,15 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_message(4, it, true);
|
buffer.encode_message(4, it, true);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(5, this->is_event);
|
buffer.encode_bool(5, this->is_event);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
buffer.encode_uint32(6, this->call_id);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
buffer.encode_bool(7, this->wants_response);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
buffer.encode_string(8, this->response_template);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
||||||
size.add_length(1, this->service_ref_.size());
|
size.add_length(1, this->service_ref_.size());
|
||||||
@@ -891,6 +900,48 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_repeated_message(1, this->data_template);
|
size.add_repeated_message(1, this->data_template);
|
||||||
size.add_repeated_message(1, this->variables);
|
size.add_repeated_message(1, this->variables);
|
||||||
size.add_bool(1, this->is_event);
|
size.add_bool(1, this->is_event);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
size.add_uint32(1, this->call_id);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
size.add_bool(1, this->wants_response);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
size.add_length(1, this->response_template.size());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1:
|
||||||
|
this->call_id = value.as_uint32();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this->success = value.as_bool();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 3:
|
||||||
|
this->error_message = value.as_string();
|
||||||
|
break;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
case 4: {
|
||||||
|
// Use raw data directly to avoid allocation
|
||||||
|
this->response_data = value.data();
|
||||||
|
this->response_data_len = value.size();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
|
@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
|
|||||||
class HomeassistantActionRequest final : public ProtoMessage {
|
class HomeassistantActionRequest final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 35;
|
static constexpr uint8_t MESSAGE_TYPE = 35;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 113;
|
static constexpr uint8_t ESTIMATED_SIZE = 128;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "homeassistant_action_request"; }
|
const char *message_name() const override { return "homeassistant_action_request"; }
|
||||||
#endif
|
#endif
|
||||||
@@ -1114,6 +1114,15 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
|||||||
std::vector<HomeassistantServiceMap> data_template{};
|
std::vector<HomeassistantServiceMap> data_template{};
|
||||||
std::vector<HomeassistantServiceMap> variables{};
|
std::vector<HomeassistantServiceMap> variables{};
|
||||||
bool is_event{false};
|
bool is_event{false};
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
uint32_t call_id{0};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
bool wants_response{false};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
std::string response_template{};
|
||||||
|
#endif
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1123,6 +1132,30 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
|||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t MESSAGE_TYPE = 130;
|
||||||
|
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
const char *message_name() const override { return "homeassistant_action_response"; }
|
||||||
|
#endif
|
||||||
|
uint32_t call_id{0};
|
||||||
|
bool success{false};
|
||||||
|
std::string error_message{};
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
const uint8_t *response_data{nullptr};
|
||||||
|
uint16_t response_data_len{0};
|
||||||
|
#endif
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void dump_to(std::string &out) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||||
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
|
@@ -1122,6 +1122,28 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
|
|||||||
out.append("\n");
|
out.append("\n");
|
||||||
}
|
}
|
||||||
dump_field(out, "is_event", this->is_event);
|
dump_field(out, "is_event", this->is_event);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
dump_field(out, "call_id", this->call_id);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
dump_field(out, "wants_response", this->wants_response);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
dump_field(out, "response_template", this->response_template);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void HomeassistantActionResponse::dump_to(std::string &out) const {
|
||||||
|
MessageDumpHelper helper(out, "HomeassistantActionResponse");
|
||||||
|
dump_field(out, "call_id", this->call_id);
|
||||||
|
dump_field(out, "success", this->success);
|
||||||
|
dump_field(out, "error_message", this->error_message);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
out.append(" response_data: ");
|
||||||
|
out.append(format_hex_pretty(this->response_data, this->response_data_len));
|
||||||
|
out.append("\n");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
|
@@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
this->on_z_wave_proxy_request(msg);
|
this->on_z_wave_proxy_request(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
case HomeassistantActionResponse::MESSAGE_TYPE: {
|
||||||
|
HomeassistantActionResponse msg;
|
||||||
|
msg.decode(msg_data, msg_size);
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
this->on_homeassistant_action_response(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
||||||
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
@@ -9,12 +9,16 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
#include "esphome/core/version.h"
|
#include "esphome/core/version.h"
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
|
#include "homeassistant_service.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
#include "esphome/components/logger/logger.h"
|
#include "esphome/components/logger/logger.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
@@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
|||||||
client->send_homeassistant_action(call);
|
client->send_homeassistant_action(call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
|
||||||
|
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
|
||||||
|
}
|
||||||
|
|
||||||
|
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
|
||||||
|
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
||||||
|
if (it->call_id == call_id) {
|
||||||
|
auto callback = std::move(it->callback);
|
||||||
|
this->action_response_callbacks_.erase(it);
|
||||||
|
ActionResponse response(success, error_message);
|
||||||
|
callback(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||||
|
const uint8_t *response_data, size_t response_data_len) {
|
||||||
|
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
||||||
|
if (it->call_id == call_id) {
|
||||||
|
auto callback = std::move(it->callback);
|
||||||
|
this->action_response_callbacks_.erase(it);
|
||||||
|
ActionResponse response(success, error_message, response_data, response_data_len);
|
||||||
|
callback(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
#include "user_services.h"
|
#include "user_services.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
@@ -111,7 +112,17 @@ class APIServer : public Component, public Controller {
|
|||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||||
|
|
||||||
#endif
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
// Action response handling
|
||||||
|
using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
|
||||||
|
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
|
||||||
|
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||||
|
const uint8_t *response_data, size_t response_data_len);
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
#endif
|
#endif
|
||||||
@@ -187,6 +198,13 @@ class APIServer : public Component, public Controller {
|
|||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
std::vector<UserServiceDescriptor *> user_services_;
|
std::vector<UserServiceDescriptor *> user_services_;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
struct PendingActionResponse {
|
||||||
|
uint32_t call_id;
|
||||||
|
ActionResponseCallback callback;
|
||||||
|
};
|
||||||
|
std::vector<PendingActionResponse> action_response_callbacks_;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Group smaller types together
|
// Group smaller types together
|
||||||
uint16_t port_{6053};
|
uint16_t port_{6053};
|
||||||
|
@@ -3,8 +3,13 @@
|
|||||||
#include "api_server.h"
|
#include "api_server.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
|
#include <functional>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
#include "esphome/components/json/json_util.h"
|
||||||
|
#endif
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
@@ -44,9 +49,47 @@ template<typename... Ts> class TemplatableKeyValuePair {
|
|||||||
TemplatableStringValue<Ts...> value;
|
TemplatableStringValue<Ts...> value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
// Represents the response data from a Home Assistant action
|
||||||
|
class ActionResponse {
|
||||||
|
public:
|
||||||
|
ActionResponse(bool success, std::string error_message = "")
|
||||||
|
: success_(success), error_message_(std::move(error_message)) {}
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
|
||||||
|
: success_(success), error_message_(std::move(error_message)) {
|
||||||
|
if (data == nullptr || data_len == 0)
|
||||||
|
return;
|
||||||
|
this->json_document_ = json::parse_json(data, data_len);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool is_success() const { return this->success_; }
|
||||||
|
const std::string &get_error_message() const { return this->error_message_; }
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
// Get data as parsed JSON object (const version returns read-only view)
|
||||||
|
JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool success_;
|
||||||
|
std::string error_message_;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
JsonDocument json_document_;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callback type for action responses
|
||||||
|
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
|
||||||
|
#endif
|
||||||
|
|
||||||
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
|
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
|
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
|
||||||
|
this->flags_.is_event = is_event;
|
||||||
|
}
|
||||||
|
|
||||||
template<typename T> void set_service(T service) { this->service_ = service; }
|
template<typename T> void set_service(T service) { this->service_ = service; }
|
||||||
|
|
||||||
@@ -61,11 +104,29 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
this->variables_.emplace_back(std::move(key), value);
|
this->variables_.emplace_back(std::move(key), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
template<typename T> void set_response_template(T response_template) {
|
||||||
|
this->response_template_ = response_template;
|
||||||
|
this->flags_.has_response_template = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_wants_status() { this->flags_.wants_status = true; }
|
||||||
|
void set_wants_response() { this->flags_.wants_response = true; }
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
|
||||||
|
return this->success_trigger_with_response_;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
|
||||||
|
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
|
||||||
void play(Ts... x) override {
|
void play(Ts... x) override {
|
||||||
HomeassistantActionRequest resp;
|
HomeassistantActionRequest resp;
|
||||||
std::string service_value = this->service_.value(x...);
|
std::string service_value = this->service_.value(x...);
|
||||||
resp.set_service(StringRef(service_value));
|
resp.set_service(StringRef(service_value));
|
||||||
resp.is_event = this->is_event_;
|
resp.is_event = this->flags_.is_event;
|
||||||
for (auto &it : this->data_) {
|
for (auto &it : this->data_) {
|
||||||
resp.data.emplace_back();
|
resp.data.emplace_back();
|
||||||
auto &kv = resp.data.back();
|
auto &kv = resp.data.back();
|
||||||
@@ -84,18 +145,74 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
kv.set_key(StringRef(it.key));
|
kv.set_key(StringRef(it.key));
|
||||||
kv.value = it.value.value(x...);
|
kv.value = it.value.value(x...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
if (this->flags_.wants_status) {
|
||||||
|
// Generate a unique call ID for this service call
|
||||||
|
static uint32_t call_id_counter = 1;
|
||||||
|
uint32_t call_id = call_id_counter++;
|
||||||
|
resp.call_id = call_id;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
if (this->flags_.wants_response) {
|
||||||
|
resp.wants_response = true;
|
||||||
|
// Set response template if provided
|
||||||
|
if (this->flags_.has_response_template) {
|
||||||
|
std::string response_template_value = this->response_template_.value(x...);
|
||||||
|
resp.response_template = response_template_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
auto captured_args = std::make_tuple(x...);
|
||||||
|
this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
|
||||||
|
std::apply(
|
||||||
|
[this, &response](auto &&...args) {
|
||||||
|
if (response.is_success()) {
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
if (this->flags_.wants_response) {
|
||||||
|
this->success_trigger_with_response_->trigger(response.get_json(), args...);
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
this->success_trigger_->trigger(args...);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this->error_trigger_->trigger(response.get_error_message(), args...);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
captured_args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
this->parent_->send_homeassistant_action(resp);
|
this->parent_->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIServer *parent_;
|
APIServer *parent_;
|
||||||
bool is_event_;
|
|
||||||
TemplatableStringValue<Ts...> service_{};
|
TemplatableStringValue<Ts...> service_{};
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
|
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
TemplatableStringValue<Ts...> response_template_{""};
|
||||||
|
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
|
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
|
||||||
|
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
|
||||||
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
|
||||||
|
struct Flags {
|
||||||
|
uint8_t is_event : 1;
|
||||||
|
uint8_t wants_status : 1;
|
||||||
|
uint8_t wants_response : 1;
|
||||||
|
uint8_t has_response_template : 1;
|
||||||
|
uint8_t reserved : 5;
|
||||||
|
} flags_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
1
esphome/components/epaper_spi/__init__.py
Normal file
1
esphome/components/epaper_spi/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@esphome/core"]
|
80
esphome/components/epaper_spi/display.py
Normal file
80
esphome/components/epaper_spi/display.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from esphome import core, pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import display, spi
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BUSY_PIN,
|
||||||
|
CONF_DC_PIN,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_LAMBDA,
|
||||||
|
CONF_MODEL,
|
||||||
|
CONF_PAGES,
|
||||||
|
CONF_RESET_DURATION,
|
||||||
|
CONF_RESET_PIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTO_LOAD = ["split_buffer"]
|
||||||
|
DEPENDENCIES = ["spi"]
|
||||||
|
|
||||||
|
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
|
||||||
|
EPaperBase = epaper_spi_ns.class_(
|
||||||
|
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
|
||||||
|
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
|
||||||
|
|
||||||
|
MODELS = {
|
||||||
|
"7.3in-spectra-e6": EPaper7p3InSpectraE6,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
display.FULL_DISPLAY_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(EPaperBase),
|
||||||
|
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
|
||||||
|
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
|
||||||
|
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||||
|
cv.positive_time_period_milliseconds,
|
||||||
|
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(spi.spi_device_schema()),
|
||||||
|
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||||
|
)
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||||
|
"epaper_spi", require_miso=False, require_mosi=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
|
||||||
|
rhs = model.new()
|
||||||
|
var = cg.Pvariable(config[CONF_ID], rhs, model)
|
||||||
|
|
||||||
|
await display.register_display(var, config)
|
||||||
|
await spi.register_spi_device(var, config)
|
||||||
|
|
||||||
|
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||||
|
cg.add(var.set_dc_pin(dc))
|
||||||
|
|
||||||
|
if CONF_LAMBDA in config:
|
||||||
|
lambda_ = await cg.process_lambda(
|
||||||
|
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
|
||||||
|
)
|
||||||
|
cg.add(var.set_writer(lambda_))
|
||||||
|
if CONF_RESET_PIN in config:
|
||||||
|
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
|
||||||
|
cg.add(var.set_reset_pin(reset))
|
||||||
|
if CONF_BUSY_PIN in config:
|
||||||
|
busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
|
||||||
|
cg.add(var.set_busy_pin(busy))
|
||||||
|
if CONF_RESET_DURATION in config:
|
||||||
|
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
|
227
esphome/components/epaper_spi/epaper_spi.cpp
Normal file
227
esphome/components/epaper_spi/epaper_spi.cpp
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#include "epaper_spi.h"
|
||||||
|
#include <cinttypes>
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
static const char *const TAG = "epaper_spi";
|
||||||
|
|
||||||
|
static const LogString *epaper_state_to_string(EPaperState state) {
|
||||||
|
switch (state) {
|
||||||
|
case EPaperState::IDLE:
|
||||||
|
return LOG_STR("IDLE");
|
||||||
|
case EPaperState::UPDATE:
|
||||||
|
return LOG_STR("UPDATE");
|
||||||
|
case EPaperState::RESET:
|
||||||
|
return LOG_STR("RESET");
|
||||||
|
case EPaperState::INITIALISE:
|
||||||
|
return LOG_STR("INITIALISE");
|
||||||
|
case EPaperState::TRANSFER_DATA:
|
||||||
|
return LOG_STR("TRANSFER_DATA");
|
||||||
|
case EPaperState::POWER_ON:
|
||||||
|
return LOG_STR("POWER_ON");
|
||||||
|
case EPaperState::REFRESH_SCREEN:
|
||||||
|
return LOG_STR("REFRESH_SCREEN");
|
||||||
|
case EPaperState::POWER_OFF:
|
||||||
|
return LOG_STR("POWER_OFF");
|
||||||
|
case EPaperState::DEEP_SLEEP:
|
||||||
|
return LOG_STR("DEEP_SLEEP");
|
||||||
|
default:
|
||||||
|
return LOG_STR("UNKNOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::setup() {
|
||||||
|
if (!this->init_buffer_(this->get_buffer_length())) {
|
||||||
|
this->mark_failed("Failed to initialise buffer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->setup_pins_();
|
||||||
|
this->spi_setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EPaperBase::init_buffer_(size_t buffer_length) {
|
||||||
|
if (!this->buffer_.init(buffer_length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::setup_pins_() {
|
||||||
|
this->dc_pin_->setup(); // OUTPUT
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->reset_pin_->setup(); // OUTPUT
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->busy_pin_ != nullptr) {
|
||||||
|
this->busy_pin_->setup(); // INPUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
||||||
|
|
||||||
|
void EPaperBase::command(uint8_t value) {
|
||||||
|
this->start_command_();
|
||||||
|
this->write_byte(value);
|
||||||
|
this->end_command_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::data(uint8_t value) {
|
||||||
|
this->start_data_();
|
||||||
|
this->write_byte(value);
|
||||||
|
this->end_data_();
|
||||||
|
}
|
||||||
|
|
||||||
|
// write a command followed by zero or more bytes of data.
|
||||||
|
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
|
||||||
|
// [COMMAND, LENGTH, DATA...]
|
||||||
|
void EPaperBase::cmd_data(const uint8_t *data) {
|
||||||
|
const uint8_t command = data[0];
|
||||||
|
const uint8_t length = data[1];
|
||||||
|
const uint8_t *ptr = data + 2;
|
||||||
|
|
||||||
|
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
|
||||||
|
format_hex_pretty(ptr, length, '.', false).c_str());
|
||||||
|
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(command);
|
||||||
|
if (length > 0) {
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
this->write_array(ptr, length);
|
||||||
|
}
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EPaperBase::is_idle_() {
|
||||||
|
if (this->busy_pin_ == nullptr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !this->busy_pin_->digital_read();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::reset() {
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
this->disable_loop();
|
||||||
|
this->set_timeout(this->reset_duration_, [this] {
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
this->set_timeout(20, [this] { this->enable_loop(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::update() {
|
||||||
|
if (!this->state_queue_.empty()) {
|
||||||
|
ESP_LOGE(TAG, "Display update already in progress - %s",
|
||||||
|
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->state_queue_.push(EPaperState::UPDATE);
|
||||||
|
this->state_queue_.push(EPaperState::RESET);
|
||||||
|
this->state_queue_.push(EPaperState::INITIALISE);
|
||||||
|
this->state_queue_.push(EPaperState::TRANSFER_DATA);
|
||||||
|
this->state_queue_.push(EPaperState::POWER_ON);
|
||||||
|
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
|
||||||
|
this->state_queue_.push(EPaperState::POWER_OFF);
|
||||||
|
this->state_queue_.push(EPaperState::DEEP_SLEEP);
|
||||||
|
this->state_queue_.push(EPaperState::IDLE);
|
||||||
|
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::loop() {
|
||||||
|
if (this->waiting_for_idle_) {
|
||||||
|
if (this->is_idle_()) {
|
||||||
|
this->waiting_for_idle_ = false;
|
||||||
|
} else {
|
||||||
|
if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
|
||||||
|
ESP_LOGV(TAG, "Waiting for idle");
|
||||||
|
this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto state = this->state_queue_.front();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case EPaperState::IDLE:
|
||||||
|
this->disable_loop();
|
||||||
|
break;
|
||||||
|
case EPaperState::UPDATE:
|
||||||
|
this->do_update_(); // Calls ESPHome (current page) lambda
|
||||||
|
break;
|
||||||
|
case EPaperState::RESET:
|
||||||
|
this->reset();
|
||||||
|
break;
|
||||||
|
case EPaperState::INITIALISE:
|
||||||
|
this->initialise_();
|
||||||
|
break;
|
||||||
|
case EPaperState::TRANSFER_DATA:
|
||||||
|
if (!this->transfer_data()) {
|
||||||
|
return; // Not done yet, come back next loop
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EPaperState::POWER_ON:
|
||||||
|
this->power_on();
|
||||||
|
break;
|
||||||
|
case EPaperState::REFRESH_SCREEN:
|
||||||
|
this->refresh_screen();
|
||||||
|
break;
|
||||||
|
case EPaperState::POWER_OFF:
|
||||||
|
this->power_off();
|
||||||
|
break;
|
||||||
|
case EPaperState::DEEP_SLEEP:
|
||||||
|
this->deep_sleep();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->state_queue_.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::start_command_() {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperBase::end_command_() { this->disable(); }
|
||||||
|
|
||||||
|
void EPaperBase::start_data_() {
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
this->enable();
|
||||||
|
}
|
||||||
|
void EPaperBase::end_data_() { this->disable(); }
|
||||||
|
|
||||||
|
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
|
||||||
|
|
||||||
|
void EPaperBase::initialise_() {
|
||||||
|
size_t index = 0;
|
||||||
|
const auto &sequence = this->init_sequence_;
|
||||||
|
const size_t sequence_size = this->init_sequence_length_;
|
||||||
|
while (index != sequence_size) {
|
||||||
|
if (sequence_size - index < 2) {
|
||||||
|
this->mark_failed("Malformed init sequence");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto *ptr = sequence + index;
|
||||||
|
const uint8_t length = ptr[1];
|
||||||
|
if (sequence_size - index < length + 2) {
|
||||||
|
this->mark_failed("Malformed init sequence");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->cmd_data(ptr);
|
||||||
|
index += length + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->power_on();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
93
esphome/components/epaper_spi/epaper_spi.h
Normal file
93
esphome/components/epaper_spi/epaper_spi.h
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/display/display_buffer.h"
|
||||||
|
#include "esphome/components/spi/spi.h"
|
||||||
|
#include "esphome/components/split_buffer/split_buffer.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
enum class EPaperState : uint8_t {
|
||||||
|
IDLE,
|
||||||
|
UPDATE,
|
||||||
|
RESET,
|
||||||
|
INITIALISE,
|
||||||
|
TRANSFER_DATA,
|
||||||
|
POWER_ON,
|
||||||
|
REFRESH_SCREEN,
|
||||||
|
POWER_OFF,
|
||||||
|
DEEP_SLEEP,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
|
||||||
|
|
||||||
|
class EPaperBase : public display::DisplayBuffer,
|
||||||
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||||
|
spi::DATA_RATE_2MHZ> {
|
||||||
|
public:
|
||||||
|
EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
|
||||||
|
: init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
|
||||||
|
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||||
|
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
|
||||||
|
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
|
||||||
|
|
||||||
|
void command(uint8_t value);
|
||||||
|
void data(uint8_t value);
|
||||||
|
void cmd_data(const uint8_t *data);
|
||||||
|
|
||||||
|
void update() override;
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
void on_safe_shutdown() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool is_idle_();
|
||||||
|
void setup_pins_();
|
||||||
|
virtual void reset();
|
||||||
|
void initialise_();
|
||||||
|
bool init_buffer_(size_t buffer_length);
|
||||||
|
|
||||||
|
virtual int get_width_controller() { return this->get_width_internal(); };
|
||||||
|
virtual void deep_sleep() = 0;
|
||||||
|
/**
|
||||||
|
* Send data to the device via SPI
|
||||||
|
* @return true if done, false if should be called next loop
|
||||||
|
*/
|
||||||
|
virtual bool transfer_data() = 0;
|
||||||
|
virtual void refresh_screen() = 0;
|
||||||
|
|
||||||
|
virtual void power_on() = 0;
|
||||||
|
virtual void power_off() = 0;
|
||||||
|
virtual uint32_t get_buffer_length() = 0;
|
||||||
|
|
||||||
|
void start_command_();
|
||||||
|
void end_command_();
|
||||||
|
void start_data_();
|
||||||
|
void end_data_();
|
||||||
|
|
||||||
|
const size_t init_sequence_length_{0};
|
||||||
|
|
||||||
|
size_t current_data_index_{0};
|
||||||
|
uint32_t reset_duration_{200};
|
||||||
|
uint32_t waiting_for_idle_last_print_{0};
|
||||||
|
|
||||||
|
GPIOPin *dc_pin_;
|
||||||
|
GPIOPin *busy_pin_{nullptr};
|
||||||
|
GPIOPin *reset_pin_{nullptr};
|
||||||
|
|
||||||
|
const uint8_t *init_sequence_{nullptr};
|
||||||
|
|
||||||
|
bool waiting_for_idle_{false};
|
||||||
|
|
||||||
|
split_buffer::SplitBuffer buffer_;
|
||||||
|
|
||||||
|
std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
@@ -0,0 +1,42 @@
|
|||||||
|
#include "epaper_spi_model_7p3in_spectra_e6.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::power_on() {
|
||||||
|
ESP_LOGI(TAG, "Power on");
|
||||||
|
this->command(0x04);
|
||||||
|
this->waiting_for_idle_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::power_off() {
|
||||||
|
ESP_LOGI(TAG, "Power off");
|
||||||
|
this->command(0x02);
|
||||||
|
this->data(0x00);
|
||||||
|
this->waiting_for_idle_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::refresh_screen() {
|
||||||
|
ESP_LOGI(TAG, "Refresh");
|
||||||
|
this->command(0x12);
|
||||||
|
this->data(0x00);
|
||||||
|
this->waiting_for_idle_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::deep_sleep() {
|
||||||
|
ESP_LOGI(TAG, "Deep sleep");
|
||||||
|
this->command(0x07);
|
||||||
|
this->data(0xA5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaper7p3InSpectraE6::dump_config() {
|
||||||
|
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||||
|
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
|
||||||
|
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 esphome::epaper_spi
|
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "epaper_spi_spectra_e6.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
|
||||||
|
static constexpr const uint16_t WIDTH = 800;
|
||||||
|
static constexpr const uint16_t HEIGHT = 480;
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
// Command, data length, data
|
||||||
|
static constexpr uint8_t INIT_SEQUENCE[] = {
|
||||||
|
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
|
||||||
|
0x01, 1, 0x3F,
|
||||||
|
0x00, 2, 0x5F, 0x69,
|
||||||
|
0x03, 4, 0x00, 0x54, 0x00, 0x44,
|
||||||
|
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
|
||||||
|
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
|
||||||
|
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
|
||||||
|
0x30, 1, 0x03,
|
||||||
|
0x50, 1, 0x3F,
|
||||||
|
0x60, 2, 0x02, 0x00,
|
||||||
|
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
|
||||||
|
0x84, 1, 0x01,
|
||||||
|
0xE3, 1, 0x2F,
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
public:
|
||||||
|
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int get_width_internal() override { return WIDTH; };
|
||||||
|
int get_height_internal() override { return HEIGHT; };
|
||||||
|
|
||||||
|
void refresh_screen() override;
|
||||||
|
void power_on() override;
|
||||||
|
void power_off() override;
|
||||||
|
void deep_sleep() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
135
esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
Normal file
135
esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#include "epaper_spi_spectra_e6.h"
|
||||||
|
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
static constexpr const char *const TAG = "epaper_spi.6c";
|
||||||
|
|
||||||
|
static inline uint8_t color_to_hex(Color color) {
|
||||||
|
if (color.red > 127) {
|
||||||
|
if (color.green > 170) {
|
||||||
|
if (color.blue > 127) {
|
||||||
|
return 0x1; // White
|
||||||
|
} else {
|
||||||
|
return 0x2; // Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0x3; // Red (or Magenta)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (color.green > 127) {
|
||||||
|
if (color.blue > 127) {
|
||||||
|
return 0x5; // Cyan -> Blue
|
||||||
|
} else {
|
||||||
|
return 0x6; // Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (color.blue > 127) {
|
||||||
|
return 0x5; // Blue
|
||||||
|
} else {
|
||||||
|
return 0x0; // Black
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperSpectraE6::fill(Color color) {
|
||||||
|
uint8_t pixel_color;
|
||||||
|
if (color.is_on()) {
|
||||||
|
pixel_color = color_to_hex(color);
|
||||||
|
} else {
|
||||||
|
pixel_color = 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We store 8 bitset<3> in 3 bytes
|
||||||
|
// | byte 1 | byte 2 | byte 3 |
|
||||||
|
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||||
|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
|
||||||
|
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
|
||||||
|
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
|
||||||
|
|
||||||
|
const size_t buffer_length = this->get_buffer_length();
|
||||||
|
for (size_t i = 0; i < buffer_length; i += 3) {
|
||||||
|
this->buffer_[i + 0] = byte_1;
|
||||||
|
this->buffer_[i + 1] = byte_2;
|
||||||
|
this->buffer_[i + 2] = byte_3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t EPaperSpectraE6::get_buffer_length() {
|
||||||
|
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
|
||||||
|
return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||||
|
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint8_t pixel_bits = color_to_hex(color);
|
||||||
|
uint32_t pixel_position = x + y * this->get_width_controller();
|
||||||
|
uint32_t first_bit_position = pixel_position * 3;
|
||||||
|
uint32_t byte_position = first_bit_position / 8u;
|
||||||
|
uint32_t byte_subposition = first_bit_position % 8u;
|
||||||
|
|
||||||
|
if (byte_subposition <= 5) {
|
||||||
|
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
|
||||||
|
(pixel_bits << (5 - byte_subposition));
|
||||||
|
} else {
|
||||||
|
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
|
||||||
|
(pixel_bits >> (byte_subposition - 5));
|
||||||
|
|
||||||
|
this->buffer_[byte_position + 1] =
|
||||||
|
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
|
||||||
|
(pixel_bits << (13 - byte_subposition));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HOT EPaperSpectraE6::transfer_data() {
|
||||||
|
const uint32_t start_time = App.get_loop_component_start_time();
|
||||||
|
if (this->current_data_index_ == 0) {
|
||||||
|
ESP_LOGV(TAG, "Sending data");
|
||||||
|
this->command(0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t bytes_to_send[4]{0};
|
||||||
|
const size_t buffer_length = this->get_buffer_length();
|
||||||
|
for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
|
||||||
|
const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
|
||||||
|
// 8 pixels are stored in 3 bytes
|
||||||
|
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||||
|
// | byte 1 | byte 2 | byte 3 |
|
||||||
|
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
|
||||||
|
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
|
||||||
|
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
|
||||||
|
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
|
||||||
|
|
||||||
|
this->start_data_();
|
||||||
|
this->write_array(bytes_to_send, sizeof(bytes_to_send));
|
||||||
|
this->end_data_();
|
||||||
|
|
||||||
|
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||||
|
// Let the main loop run and come back next loop
|
||||||
|
this->current_data_index_ = i + 3;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Finished the entire dataset
|
||||||
|
this->current_data_index_ = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperSpectraE6::reset() {
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->disable_loop();
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
this->set_timeout(20, [this] {
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
delay(2);
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
this->set_timeout(20, [this] { this->enable_loop(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
23
esphome/components/epaper_spi/epaper_spi_spectra_e6.h
Normal file
23
esphome/components/epaper_spi/epaper_spi_spectra_e6.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "epaper_spi.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
class EPaperSpectraE6 : public EPaperBase {
|
||||||
|
public:
|
||||||
|
EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
|
||||||
|
: EPaperBase(init_sequence, init_sequence_length) {}
|
||||||
|
|
||||||
|
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||||
|
void fill(Color color) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||||
|
uint32_t get_buffer_length() override;
|
||||||
|
|
||||||
|
bool transfer_data() override;
|
||||||
|
void reset() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
5
esphome/components/split_buffer/__init__.py
Normal file
5
esphome/components/split_buffer/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
CODEOWNERS = ["@jesserockz"]
|
||||||
|
|
||||||
|
# Allows split_buffer to be configured in yaml, to allow use of the C++ api.
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = {}
|
133
esphome/components/split_buffer/split_buffer.cpp
Normal file
133
esphome/components/split_buffer/split_buffer.cpp
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#include "split_buffer.h"
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::split_buffer {
|
||||||
|
|
||||||
|
static constexpr const char *const TAG = "split_buffer";
|
||||||
|
|
||||||
|
SplitBuffer::~SplitBuffer() { this->free(); }
|
||||||
|
|
||||||
|
bool SplitBuffer::init(size_t total_length) {
|
||||||
|
this->free(); // Clean up any existing allocation
|
||||||
|
|
||||||
|
if (total_length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->total_length_ = total_length;
|
||||||
|
size_t current_buffer_size = total_length;
|
||||||
|
|
||||||
|
RAMAllocator<uint8_t *> ptr_allocator;
|
||||||
|
RAMAllocator<uint8_t> allocator;
|
||||||
|
|
||||||
|
// Try to allocate the entire buffer first
|
||||||
|
while (current_buffer_size > 0) {
|
||||||
|
// Calculate how many buffers we need of this size
|
||||||
|
size_t needed_buffers = (total_length + current_buffer_size - 1) / current_buffer_size;
|
||||||
|
|
||||||
|
// Try to allocate array of buffer pointers
|
||||||
|
uint8_t **temp_buffers = ptr_allocator.allocate(needed_buffers);
|
||||||
|
if (temp_buffers == nullptr) {
|
||||||
|
// If we can't even allocate the pointer array, don't need to continue
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate pointers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize all pointers to null
|
||||||
|
for (size_t i = 0; i < needed_buffers; i++) {
|
||||||
|
temp_buffers[i] = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to allocate all the buffers
|
||||||
|
bool allocation_success = true;
|
||||||
|
for (size_t i = 0; i < needed_buffers; i++) {
|
||||||
|
size_t this_buffer_size = current_buffer_size;
|
||||||
|
// Last buffer might be smaller if total_length is not divisible by current_buffer_size
|
||||||
|
if (i == needed_buffers - 1 && total_length % current_buffer_size != 0) {
|
||||||
|
this_buffer_size = total_length % current_buffer_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
temp_buffers[i] = allocator.allocate(this_buffer_size);
|
||||||
|
if (temp_buffers[i] == nullptr) {
|
||||||
|
allocation_success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize buffer to zero
|
||||||
|
memset(temp_buffers[i], 0, this_buffer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allocation_success) {
|
||||||
|
// Success! Store the result
|
||||||
|
this->buffers_ = temp_buffers;
|
||||||
|
this->buffer_count_ = needed_buffers;
|
||||||
|
this->buffer_size_ = current_buffer_size;
|
||||||
|
ESP_LOGD(TAG, "Allocated %zu * %zu bytes - %zu bytes", this->buffer_count_, this->buffer_size_,
|
||||||
|
this->total_length_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocation failed, clean up and try smaller buffers
|
||||||
|
for (size_t i = 0; i < needed_buffers; i++) {
|
||||||
|
if (temp_buffers[i] != nullptr) {
|
||||||
|
allocator.deallocate(temp_buffers[i], 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ptr_allocator.deallocate(temp_buffers, 0);
|
||||||
|
|
||||||
|
// Halve the buffer size and try again
|
||||||
|
current_buffer_size = current_buffer_size / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate %zu bytes", total_length);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SplitBuffer::free() {
|
||||||
|
if (this->buffers_ != nullptr) {
|
||||||
|
RAMAllocator<uint8_t> allocator;
|
||||||
|
for (size_t i = 0; i < this->buffer_count_; i++) {
|
||||||
|
if (this->buffers_[i] != nullptr) {
|
||||||
|
allocator.deallocate(this->buffers_[i], 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RAMAllocator<uint8_t *> ptr_allocator;
|
||||||
|
ptr_allocator.deallocate(this->buffers_, 0);
|
||||||
|
this->buffers_ = nullptr;
|
||||||
|
}
|
||||||
|
this->buffer_count_ = 0;
|
||||||
|
this->buffer_size_ = 0;
|
||||||
|
this->total_length_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t &SplitBuffer::operator[](size_t index) {
|
||||||
|
if (index >= this->total_length_) {
|
||||||
|
ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_);
|
||||||
|
// Return reference to a static dummy byte to avoid crash
|
||||||
|
static uint8_t dummy = 0;
|
||||||
|
return dummy;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t buffer_index = index / this->buffer_size_;
|
||||||
|
size_t offset_in_buffer = index - this->buffer_size_ * buffer_index;
|
||||||
|
|
||||||
|
return this->buffers_[buffer_index][offset_in_buffer];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t &SplitBuffer::operator[](size_t index) const {
|
||||||
|
if (index >= this->total_length_) {
|
||||||
|
ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_);
|
||||||
|
// Return reference to a static dummy byte to avoid crash
|
||||||
|
static const uint8_t DUMMY = 0;
|
||||||
|
return DUMMY;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t buffer_index = index / this->buffer_size_;
|
||||||
|
size_t offset_in_buffer = index - this->buffer_size_ * buffer_index;
|
||||||
|
|
||||||
|
return this->buffers_[buffer_index][offset_in_buffer];
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::split_buffer
|
40
esphome/components/split_buffer/split_buffer.h
Normal file
40
esphome/components/split_buffer/split_buffer.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace esphome::split_buffer {
|
||||||
|
|
||||||
|
class SplitBuffer {
|
||||||
|
public:
|
||||||
|
SplitBuffer() = default;
|
||||||
|
~SplitBuffer();
|
||||||
|
|
||||||
|
// Initialize the buffer with the desired total length
|
||||||
|
bool init(size_t total_length);
|
||||||
|
|
||||||
|
// Free all allocated buffers
|
||||||
|
void free();
|
||||||
|
|
||||||
|
// Access operators
|
||||||
|
uint8_t &operator[](size_t index);
|
||||||
|
const uint8_t &operator[](size_t index) const;
|
||||||
|
|
||||||
|
// Get the total length
|
||||||
|
size_t size() const { return this->total_length_; }
|
||||||
|
|
||||||
|
// Get buffer information
|
||||||
|
size_t get_buffer_count() const { return this->buffer_count_; }
|
||||||
|
size_t get_buffer_size() const { return this->buffer_size_; }
|
||||||
|
|
||||||
|
// Check if successfully initialized
|
||||||
|
bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t **buffers_{nullptr};
|
||||||
|
size_t buffer_count_{0};
|
||||||
|
size_t buffer_size_{0};
|
||||||
|
size_t total_length_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::split_buffer
|
@@ -677,6 +677,7 @@ CONF_ON_RESPONSE = "on_response"
|
|||||||
CONF_ON_SHUTDOWN = "on_shutdown"
|
CONF_ON_SHUTDOWN = "on_shutdown"
|
||||||
CONF_ON_SPEED_SET = "on_speed_set"
|
CONF_ON_SPEED_SET = "on_speed_set"
|
||||||
CONF_ON_STATE = "on_state"
|
CONF_ON_STATE = "on_state"
|
||||||
|
CONF_ON_SUCCESS = "on_success"
|
||||||
CONF_ON_TAG = "on_tag"
|
CONF_ON_TAG = "on_tag"
|
||||||
CONF_ON_TAG_REMOVED = "on_tag_removed"
|
CONF_ON_TAG_REMOVED = "on_tag_removed"
|
||||||
CONF_ON_TIME = "on_time"
|
CONF_ON_TIME = "on_time"
|
||||||
@@ -819,6 +820,7 @@ CONF_RESET_DURATION = "reset_duration"
|
|||||||
CONF_RESET_PIN = "reset_pin"
|
CONF_RESET_PIN = "reset_pin"
|
||||||
CONF_RESIZE = "resize"
|
CONF_RESIZE = "resize"
|
||||||
CONF_RESOLUTION = "resolution"
|
CONF_RESOLUTION = "resolution"
|
||||||
|
CONF_RESPONSE_TEMPLATE = "response_template"
|
||||||
CONF_RESTART = "restart"
|
CONF_RESTART = "restart"
|
||||||
CONF_RESTORE = "restore"
|
CONF_RESTORE = "restore"
|
||||||
CONF_RESTORE_MODE = "restore_mode"
|
CONF_RESTORE_MODE = "restore_mode"
|
||||||
|
@@ -113,6 +113,8 @@
|
|||||||
#define USE_API
|
#define USE_API
|
||||||
#define USE_API_CLIENT_CONNECTED_TRIGGER
|
#define USE_API_CLIENT_CONNECTED_TRIGGER
|
||||||
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
|
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
|
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
|
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
#define USE_API_HOMEASSISTANT_SERVICES
|
#define USE_API_HOMEASSISTANT_SERVICES
|
||||||
#define USE_API_HOMEASSISTANT_STATES
|
#define USE_API_HOMEASSISTANT_STATES
|
||||||
#define USE_API_NOISE
|
#define USE_API_NOISE
|
||||||
|
@@ -10,6 +10,39 @@ esphome:
|
|||||||
data:
|
data:
|
||||||
message: Button was pressed
|
message: Button was pressed
|
||||||
- homeassistant.tag_scanned: pulse
|
- homeassistant.tag_scanned: pulse
|
||||||
|
- homeassistant.action:
|
||||||
|
action: weather.get_forecasts
|
||||||
|
data:
|
||||||
|
entity_id: weather.forecast_home
|
||||||
|
type: hourly
|
||||||
|
capture_response: true
|
||||||
|
on_success:
|
||||||
|
- lambda: |-
|
||||||
|
JsonObjectConst next_hour = response["response"]["weather.forecast_home"]["forecast"][0];
|
||||||
|
float next_temperature = next_hour["temperature"].as<float>();
|
||||||
|
ESP_LOGD("main", "Next hour temperature: %f", next_temperature);
|
||||||
|
on_error:
|
||||||
|
- lambda: |-
|
||||||
|
ESP_LOGE("main", "Action failed with error: %s", error.c_str());
|
||||||
|
- homeassistant.action:
|
||||||
|
action: weather.get_forecasts
|
||||||
|
data:
|
||||||
|
entity_id: weather.forecast_home
|
||||||
|
type: hourly
|
||||||
|
capture_response: true
|
||||||
|
response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}"
|
||||||
|
on_success:
|
||||||
|
- lambda: |-
|
||||||
|
float temperature = response["response"].as<float>();
|
||||||
|
ESP_LOGD("main", "Next hour temperature: %f", temperature);
|
||||||
|
- homeassistant.action:
|
||||||
|
action: light.toggle
|
||||||
|
data:
|
||||||
|
entity_id: light.demo_light
|
||||||
|
on_success:
|
||||||
|
- logger.log: "Toggled demo light"
|
||||||
|
on_error:
|
||||||
|
- logger.log: "Failed to toggle demo light"
|
||||||
|
|
||||||
api:
|
api:
|
||||||
port: 8000
|
port: 8000
|
||||||
|
15
tests/components/epaper_spi/test.esp32-s3-idf.yaml
Normal file
15
tests/components/epaper_spi/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
spi:
|
||||||
|
clk_pin: GPIO7
|
||||||
|
mosi_pin: GPIO9
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: epaper_spi
|
||||||
|
model: 7.3in-spectra-e6
|
||||||
|
cs_pin: GPIO5
|
||||||
|
dc_pin: GPIO17
|
||||||
|
reset_pin: GPIO16
|
||||||
|
busy_pin: GPIO4
|
||||||
|
rotation: 0
|
||||||
|
update_interval: 60s
|
||||||
|
lambda: |-
|
||||||
|
it.circle(64, 64, 50, Color::BLACK);
|
Reference in New Issue
Block a user