mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a0ca17fb8 | ||
|
|
98c456f21a | ||
|
|
3ddca1fa34 | ||
|
|
77f5369d39 | ||
|
|
1fef5b8e7d | ||
|
|
ab4896990e | ||
|
|
85fa295c77 | ||
|
|
cdff148993 | ||
|
|
0aaa8168ae | ||
|
|
d76d5655ff |
No files matched your search
@@ -31,14 +31,6 @@ def validate_entity_domain(
|
||||
return validator
|
||||
|
||||
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ENTITY_ID): cv.entity_id,
|
||||
cv.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
cv.Optional(CONF_INTERNAL, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ENTITY_ID): cv.entity_id,
|
||||
@@ -46,6 +38,12 @@ HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA = HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def final_validate_entity_id(config: ConfigType) -> ConfigType:
|
||||
"""Without an entity_id, the entity must be a wizard input, as Home Assistant then supplies it."""
|
||||
@@ -82,8 +80,14 @@ async def _emit_state_subscription_count_define() -> None:
|
||||
|
||||
|
||||
def setup_home_assistant_entity(
|
||||
var: MockObj, config: ConfigType, state_subscription_count: int = 1
|
||||
var: MockObj,
|
||||
config: ConfigType,
|
||||
state_subscription_count: int = 1,
|
||||
services: bool = False,
|
||||
) -> None:
|
||||
"""Set up an imported entity; services for one that sends actions."""
|
||||
if services:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
# A wizard input has no entity_id; the api wizard sets it
|
||||
cg.add_optional(var.set_entity_id, config.get(CONF_ENTITY_ID))
|
||||
cg.add_optional(var.set_attribute, config.get(CONF_ATTRIBUTE))
|
||||
|
||||
@@ -11,32 +11,22 @@ ESPHOME_LOG_TAG(TAG, "homeassistant.binary_sensor");
|
||||
void HomeassistantBinarySensor::setup() {
|
||||
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, this->attribute_, [this](StringRef state) {
|
||||
auto val = parse_on_off(state.c_str());
|
||||
switch (val) {
|
||||
case PARSE_NONE:
|
||||
case PARSE_TOGGLE:
|
||||
// Home Assistant sends these when there is no value, which is not an error
|
||||
if (is_unset(state)) {
|
||||
ESP_LOGV(TAG, "'%s': No value (%s)", this->entity_id_, state.c_str());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str());
|
||||
}
|
||||
break;
|
||||
case PARSE_ON:
|
||||
case PARSE_OFF:
|
||||
bool new_state = val == PARSE_ON;
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_, this->attribute_, ONOFF(new_state));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
|
||||
}
|
||||
if (this->initial_) {
|
||||
this->publish_initial_state(new_state);
|
||||
} else {
|
||||
this->publish_state(new_state);
|
||||
}
|
||||
break;
|
||||
if (val != PARSE_ON && val != PARSE_OFF) {
|
||||
// Home Assistant sends these when there is no value, which is not an error
|
||||
if (is_unset(state)) {
|
||||
ESP_LOGV(TAG, "'%s': No value (%s)", this->entity_id_, state.c_str());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
this->initial_ = false;
|
||||
bool new_state = val == PARSE_ON;
|
||||
if (this->attribute_ != nullptr) {
|
||||
ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_, this->attribute_, ONOFF(new_state));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
|
||||
}
|
||||
this->publish_state(new_state);
|
||||
});
|
||||
}
|
||||
void HomeassistantBinarySensor::dump_config() {
|
||||
|
||||
@@ -16,7 +16,6 @@ class HomeassistantBinarySensor final : public binary_sensor::BinarySensor, publ
|
||||
protected:
|
||||
const char *entity_id_{nullptr};
|
||||
const char *attribute_{nullptr};
|
||||
bool initial_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::homeassistant
|
||||
@@ -1,11 +1,32 @@
|
||||
#include "homeassistant.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::homeassistant {
|
||||
|
||||
PROGMEM_STRING_TABLE(UnsetValues, "None", "unavailable", "unknown");
|
||||
|
||||
bool is_unset(StringRef value) { return progmem_string_table_find<UnsetValues>(value.c_str(), value.size()) >= 0; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
bool can_send_action(const char *tag, const char *entity_id, [[maybe_unused]] EntityBase *entity) {
|
||||
if (!api::global_api_server->is_connected()) {
|
||||
ESP_LOGE(tag, "'%s': No clients connected to API server", entity_id);
|
||||
return false;
|
||||
}
|
||||
#ifdef USE_API_WIZARD_LINKED_INPUTS
|
||||
if (entity_id[0] == '\0') {
|
||||
ESP_LOGW(tag, "'%s': No entity ID set yet", LOG_STR_ARG(entity->get_log_name()));
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::homeassistant
|
||||
@@ -2,15 +2,40 @@
|
||||
|
||||
#include <cctype>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::homeassistant {
|
||||
|
||||
/// True for what Home Assistant reports when there is no value: "None" for an attribute that is not
|
||||
/// set, "unavailable" and "unknown" for the state of an entity that is not ready
|
||||
bool is_unset(StringRef value);
|
||||
|
||||
/// True if entity_id is an input helper; input_service must start with "input_"
|
||||
inline bool is_input_helper(const char *entity_id, const char *input_service) {
|
||||
return ESPHOME_strncmp_P(entity_id, input_service, sizeof("input_") - 1) == 0;
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
/// Logs and returns false when no API client is connected or no entity is linked yet
|
||||
bool can_send_action(const char *tag, const char *entity_id, EntityBase *entity);
|
||||
|
||||
/// Adds the entity_id entry to an action's data
|
||||
inline void add_entity_id(api::HomeassistantActionRequest &resp, const char *entity_id) {
|
||||
static constexpr char ENTITY_ID_KEY[] PROGMEM = "entity_id";
|
||||
auto &entry = resp.data.emplace_back();
|
||||
entry.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
|
||||
entry.value = ProgmemStringRef(StringRef(entity_id));
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Emit the UTF-8 encoding of a code point. Rejects NUL, which would end the item early.
|
||||
template<typename C> bool emit_utf8(uint32_t code_point, C &on_char) {
|
||||
if (code_point == 0 || code_point > 0x10FFFF)
|
||||
|
||||
@@ -72,10 +72,9 @@ FINAL_VALIDATE_SCHEMA = final_validate_entity_id
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = await light.new_light(config)
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_report_timeout(config[CONF_REPORT_TIMEOUT].total_seconds))
|
||||
# 1 state + supported_color_modes, min/max_color_temp_kelvin, color_mode, brightness,
|
||||
# color_temp_kelvin and rgb_color in HomeassistantLight::setup()
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=8)
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=8, services=True)
|
||||
@@ -25,7 +25,7 @@ namespace {
|
||||
// color mode, all handled as RGB: Home Assistant reports rgb_color for all of them and derives
|
||||
// the white channels from rgb_color itself.
|
||||
PROGMEM_STRING_TABLE(ColorModeNames, "onoff", "brightness", "white", "color_temp", "hs", "xy", "rgb", "rgbw", "rgbww");
|
||||
const uint8_t COLOR_MODES[] PROGMEM = {
|
||||
constexpr uint8_t COLOR_MODES[] PROGMEM = {
|
||||
static_cast<uint8_t>(ColorMode::ON_OFF),
|
||||
static_cast<uint8_t>(ColorMode::BRIGHTNESS),
|
||||
static_cast<uint8_t>(ColorMode::WHITE),
|
||||
@@ -381,27 +381,18 @@ void HomeassistantLight::write_state(LightState *state) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!api::global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "'%s': No clients connected to API server, change lost", this->entity_id_);
|
||||
if (!can_send_action(TAG, this->entity_id_, state)) {
|
||||
this->fall_back_();
|
||||
return;
|
||||
}
|
||||
#ifdef USE_API_WIZARD_LINKED_INPUTS
|
||||
if (this->entity_id_[0] == '\0') {
|
||||
ESP_LOGW(TAG, "'%s': No entity ID set yet", LOG_STR_ARG(state->get_log_name()));
|
||||
this->fall_back_();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// In flash on the ESP8266 and encoded from there
|
||||
static const char SERVICE_TURN_ON[] PROGMEM = "light.turn_on";
|
||||
static const char SERVICE_TURN_OFF[] PROGMEM = "light.turn_off";
|
||||
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
|
||||
static const char BRIGHTNESS_KEY[] PROGMEM = "brightness";
|
||||
static const char WHITE_KEY[] PROGMEM = "white";
|
||||
static const char COLOR_TEMP_KELVIN_KEY[] PROGMEM = "color_temp_kelvin";
|
||||
static const char RGB_COLOR_KEY[] PROGMEM = "rgb_color";
|
||||
static constexpr char SERVICE_TURN_ON[] PROGMEM = "light.turn_on";
|
||||
static constexpr char SERVICE_TURN_OFF[] PROGMEM = "light.turn_off";
|
||||
static constexpr char BRIGHTNESS_KEY[] PROGMEM = "brightness";
|
||||
static constexpr char WHITE_KEY[] PROGMEM = "white";
|
||||
static constexpr char COLOR_TEMP_KELVIN_KEY[] PROGMEM = "color_temp_kelvin";
|
||||
static constexpr char RGB_COLOR_KEY[] PROGMEM = "rgb_color";
|
||||
|
||||
// The request refers to these buffers until it is sent below
|
||||
char level_buf[4]; // up to 255
|
||||
@@ -410,9 +401,7 @@ void HomeassistantLight::write_state(LightState *state) {
|
||||
|
||||
api::HomeassistantActionRequest resp;
|
||||
resp.data.init(3);
|
||||
auto &entity_id = resp.data.emplace_back();
|
||||
entity_id.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
|
||||
entity_id.value = ProgmemStringRef(StringRef(this->entity_id_));
|
||||
add_entity_id(resp, this->entity_id_);
|
||||
|
||||
auto add_number = [&](ProgmemStringRef key, char *buf, size_t size, unsigned number) {
|
||||
auto &entry = resp.data.emplace_back();
|
||||
|
||||
@@ -13,6 +13,7 @@ from .. import (
|
||||
|
||||
CODEOWNERS = ["@landonr"]
|
||||
DEPENDENCIES = ["api"]
|
||||
AUTO_LOAD = ["homeassistant"]
|
||||
|
||||
SUPPORTED_DOMAINS = ["input_number", "number"]
|
||||
|
||||
@@ -31,7 +32,6 @@ FINAL_VALIDATE_SCHEMA = final_validate_entity_id
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = await number.new_number(
|
||||
config,
|
||||
min_value=0,
|
||||
@@ -40,4 +40,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
# 1 state + min/max/step fetches in HomeassistantNumber::setup()
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=4)
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=4, services=True)
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "homeassistant_number.h"
|
||||
|
||||
#include <cstring>
|
||||
#include "../homeassistant.h"
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
@@ -26,46 +26,32 @@ void HomeassistantNumber::state_changed_(StringRef state) {
|
||||
this->publish_state(number_value.value());
|
||||
}
|
||||
|
||||
void HomeassistantNumber::min_retrieved_(StringRef min) {
|
||||
auto min_value = parse_number<float>(min.c_str());
|
||||
if (!min_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_, min.c_str());
|
||||
return;
|
||||
optional<float> HomeassistantNumber::parse_attribute_(const char *name, StringRef value) {
|
||||
auto number = parse_number<float>(value.c_str());
|
||||
if (!number.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert '%s' value '%s' to number!", this->entity_id_, name, value.c_str());
|
||||
return nullopt;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Min retrieved: %s", LOG_STR_ARG(this->get_log_name()), min.c_str());
|
||||
this->traits.set_min_value(min_value.value());
|
||||
}
|
||||
|
||||
void HomeassistantNumber::max_retrieved_(StringRef max) {
|
||||
auto max_value = parse_number<float>(max.c_str());
|
||||
if (!max_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_, max.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Max retrieved: %s", LOG_STR_ARG(this->get_log_name()), max.c_str());
|
||||
this->traits.set_max_value(max_value.value());
|
||||
}
|
||||
|
||||
void HomeassistantNumber::step_retrieved_(StringRef step) {
|
||||
auto step_value = parse_number<float>(step.c_str());
|
||||
if (!step_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_, step.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Step Retrieved %s", LOG_STR_ARG(this->get_log_name()), step.c_str());
|
||||
this->traits.set_step(step_value.value());
|
||||
ESP_LOGD(TAG, "'%s': Retrieved '%s': %s", LOG_STR_ARG(this->get_log_name()), name, value.c_str());
|
||||
return number;
|
||||
}
|
||||
|
||||
void HomeassistantNumber::setup() {
|
||||
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr,
|
||||
[this](StringRef state) { this->state_changed_(state); });
|
||||
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "min",
|
||||
[this](StringRef min) { this->min_retrieved_(min); });
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "max",
|
||||
[this](StringRef max) { this->max_retrieved_(max); });
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "step",
|
||||
[this](StringRef step) { this->step_retrieved_(step); });
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "min", [this](StringRef value) {
|
||||
if (auto min = this->parse_attribute_("min", value))
|
||||
this->traits.set_min_value(*min);
|
||||
});
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "max", [this](StringRef value) {
|
||||
if (auto max = this->parse_attribute_("max", value))
|
||||
this->traits.set_max_value(*max);
|
||||
});
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "step", [this](StringRef value) {
|
||||
if (auto step = this->parse_attribute_("step", value))
|
||||
this->traits.set_step(*step);
|
||||
});
|
||||
}
|
||||
|
||||
void HomeassistantNumber::dump_config() {
|
||||
@@ -76,36 +62,23 @@ void HomeassistantNumber::dump_config() {
|
||||
float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
void HomeassistantNumber::control(float value) {
|
||||
if (!api::global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "No clients connected to API server");
|
||||
if (!can_send_action(TAG, this->entity_id_, this))
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_API_WIZARD_LINKED_INPUTS
|
||||
if (this->entity_id_[0] == '\0') {
|
||||
ESP_LOGW(TAG, "'%s': No entity ID set yet", LOG_STR_ARG(this->get_log_name()));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->publish_state(value);
|
||||
|
||||
// In flash on the ESP8266 and encoded from there
|
||||
static const char NUMBER_SERVICE_NAME[] PROGMEM = "number.set_value";
|
||||
static const char INPUT_NUMBER_SERVICE_NAME[] PROGMEM = "input_number.set_value";
|
||||
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
|
||||
static const char VALUE_KEY[] PROGMEM = "value";
|
||||
static constexpr char NUMBER_SERVICE_NAME[] PROGMEM = "number.set_value";
|
||||
static constexpr char INPUT_NUMBER_SERVICE_NAME[] PROGMEM = "input_number.set_value";
|
||||
static constexpr char VALUE_KEY[] PROGMEM = "value";
|
||||
|
||||
api::HomeassistantActionRequest resp;
|
||||
// An input helper entity id starts with the "input_" its action name starts with
|
||||
const bool input_helper = ESPHOME_strncmp_P(this->entity_id_, INPUT_NUMBER_SERVICE_NAME, sizeof("input_") - 1) == 0;
|
||||
resp.service = input_helper ? ProgmemStringRef::from_lit(INPUT_NUMBER_SERVICE_NAME)
|
||||
: ProgmemStringRef::from_lit(NUMBER_SERVICE_NAME);
|
||||
resp.service = is_input_helper(this->entity_id_, INPUT_NUMBER_SERVICE_NAME)
|
||||
? ProgmemStringRef::from_lit(INPUT_NUMBER_SERVICE_NAME)
|
||||
: ProgmemStringRef::from_lit(NUMBER_SERVICE_NAME);
|
||||
|
||||
resp.data.init(2);
|
||||
auto &entity_id = resp.data.emplace_back();
|
||||
entity_id.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
|
||||
entity_id.value = ProgmemStringRef(StringRef(this->entity_id_));
|
||||
add_entity_id(resp, this->entity_id_);
|
||||
|
||||
auto &entity_value = resp.data.emplace_back();
|
||||
entity_value.key = ProgmemStringRef::from_lit(VALUE_KEY);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::homeassistant {
|
||||
@@ -16,9 +17,7 @@ class HomeassistantNumber final : public number::Number, public Component {
|
||||
|
||||
protected:
|
||||
void state_changed_(StringRef state);
|
||||
void min_retrieved_(StringRef min);
|
||||
void max_retrieved_(StringRef max);
|
||||
void step_retrieved_(StringRef step);
|
||||
optional<float> parse_attribute_(const char *name, StringRef value);
|
||||
|
||||
void control(float value) override;
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ FINAL_VALIDATE_SCHEMA = final_validate_entity_id
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = await select.new_select(
|
||||
config,
|
||||
cg.static_buffer(config[CONF_ID], cg.char, config[CONF_OPTIONS_BUFFER_SIZE]),
|
||||
@@ -56,4 +55,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
# 1 state + options fetch in HomeassistantSelect::setup()
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=2)
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=2, services=True)
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "homeassistant_select.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "../homeassistant.h"
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
@@ -113,34 +111,21 @@ void HomeassistantSelect::dump_config() {
|
||||
float HomeassistantSelect::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
void HomeassistantSelect::control(size_t index) {
|
||||
if (!api::global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "No clients connected to API server");
|
||||
if (!can_send_action(TAG, this->entity_id_, this))
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_API_WIZARD_LINKED_INPUTS
|
||||
if (this->entity_id_[0] == '\0') {
|
||||
ESP_LOGW(TAG, "'%s': No entity ID set yet", LOG_STR_ARG(this->get_log_name()));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// In flash on the ESP8266 and encoded from there
|
||||
static const char SERVICE_SELECT[] PROGMEM = "select.select_option";
|
||||
static const char SERVICE_INPUT_SELECT[] PROGMEM = "input_select.select_option";
|
||||
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
|
||||
static const char OPTION_KEY[] PROGMEM = "option";
|
||||
static constexpr char SERVICE_SELECT[] PROGMEM = "select.select_option";
|
||||
static constexpr char SERVICE_INPUT_SELECT[] PROGMEM = "input_select.select_option";
|
||||
static constexpr char OPTION_KEY[] PROGMEM = "option";
|
||||
|
||||
api::HomeassistantActionRequest resp;
|
||||
// An input helper entity id starts with the "input_" its action name starts with
|
||||
const bool input_helper = ESPHOME_strncmp_P(this->entity_id_, SERVICE_INPUT_SELECT, sizeof("input_") - 1) == 0;
|
||||
resp.service =
|
||||
input_helper ? ProgmemStringRef::from_lit(SERVICE_INPUT_SELECT) : ProgmemStringRef::from_lit(SERVICE_SELECT);
|
||||
resp.service = is_input_helper(this->entity_id_, SERVICE_INPUT_SELECT)
|
||||
? ProgmemStringRef::from_lit(SERVICE_INPUT_SELECT)
|
||||
: ProgmemStringRef::from_lit(SERVICE_SELECT);
|
||||
|
||||
resp.data.init(2);
|
||||
auto &entity_id = resp.data.emplace_back();
|
||||
entity_id.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
|
||||
entity_id.value = ProgmemStringRef(StringRef(this->entity_id_));
|
||||
add_entity_id(resp, this->entity_id_);
|
||||
|
||||
auto &option = resp.data.emplace_back();
|
||||
option.key = ProgmemStringRef::from_lit(OPTION_KEY);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
@@ -14,6 +13,7 @@ from .. import (
|
||||
|
||||
CODEOWNERS = ["@Links2004"]
|
||||
DEPENDENCIES = ["api"]
|
||||
AUTO_LOAD = ["homeassistant"]
|
||||
|
||||
SUPPORTED_DOMAINS = [
|
||||
"automation",
|
||||
@@ -42,8 +42,6 @@ FINAL_VALIDATE_SCHEMA = final_validate_entity_id
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
await switch.register_switch(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
setup_home_assistant_entity(var, config, services=True)
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "homeassistant_switch.h"
|
||||
#include "../homeassistant.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
@@ -8,23 +9,16 @@ namespace esphome::homeassistant {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "homeassistant.switch");
|
||||
|
||||
using namespace esphome::switch_;
|
||||
|
||||
void HomeassistantSwitch::setup() {
|
||||
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, [this](StringRef state) {
|
||||
auto val = parse_on_off(state.c_str());
|
||||
switch (val) {
|
||||
case PARSE_NONE:
|
||||
case PARSE_TOGGLE:
|
||||
ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str());
|
||||
break;
|
||||
case PARSE_ON:
|
||||
case PARSE_OFF:
|
||||
bool new_state = val == PARSE_ON;
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
|
||||
this->publish_state(new_state);
|
||||
break;
|
||||
if (val != PARSE_ON && val != PARSE_OFF) {
|
||||
ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str());
|
||||
return;
|
||||
}
|
||||
bool new_state = val == PARSE_ON;
|
||||
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
|
||||
this->publish_state(new_state);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,30 +30,18 @@ void HomeassistantSwitch::dump_config() {
|
||||
float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
void HomeassistantSwitch::write_state(bool state) {
|
||||
if (!api::global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "No clients connected to API server");
|
||||
if (!can_send_action(TAG, this->entity_id_, this))
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_API_WIZARD_LINKED_INPUTS
|
||||
if (this->entity_id_[0] == '\0') {
|
||||
ESP_LOGW(TAG, "'%s': No entity ID set yet", LOG_STR_ARG(this->get_log_name()));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// In flash on the ESP8266 and encoded from there
|
||||
static const char SERVICE_ON[] PROGMEM = "homeassistant.turn_on";
|
||||
static const char SERVICE_OFF[] PROGMEM = "homeassistant.turn_off";
|
||||
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
|
||||
static constexpr char SERVICE_ON[] PROGMEM = "homeassistant.turn_on";
|
||||
static constexpr char SERVICE_OFF[] PROGMEM = "homeassistant.turn_off";
|
||||
|
||||
api::HomeassistantActionRequest resp;
|
||||
resp.service = state ? ProgmemStringRef::from_lit(SERVICE_ON) : ProgmemStringRef::from_lit(SERVICE_OFF);
|
||||
|
||||
resp.data.init(1);
|
||||
auto &entity_id_kv = resp.data.emplace_back();
|
||||
entity_id_kv.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
|
||||
entity_id_kv.value = ProgmemStringRef(StringRef(this->entity_id_));
|
||||
add_entity_id(resp, this->entity_id_);
|
||||
|
||||
api::global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from .. import (
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
DEPENDENCIES = ["api"]
|
||||
AUTO_LOAD = ["homeassistant"]
|
||||
|
||||
SUPPORTED_DOMAINS = [
|
||||
"input_text",
|
||||
@@ -34,8 +35,7 @@ FINAL_VALIDATE_SCHEMA = final_validate_entity_id
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = await text.new_text(config)
|
||||
await cg.register_component(var, config)
|
||||
# 1 state + min/max/mode fetches in HomeassistantText::setup()
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=4)
|
||||
setup_home_assistant_entity(var, config, state_subscription_count=4, services=True)
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "homeassistant_text.h"
|
||||
|
||||
#include <cstring>
|
||||
#include "../homeassistant.h"
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
@@ -13,6 +13,9 @@ namespace esphome::homeassistant {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "homeassistant.text");
|
||||
|
||||
PROGMEM_STRING_TABLE(TextModeNames, "text", "password");
|
||||
static_assert(text::TEXT_MODE_TEXT == 0 && text::TEXT_MODE_PASSWORD == 1 && TextModeNames::COUNT == 2);
|
||||
|
||||
void HomeassistantText::state_changed_(StringRef state) {
|
||||
if (state == this->state) {
|
||||
return;
|
||||
@@ -21,35 +24,23 @@ void HomeassistantText::state_changed_(StringRef state) {
|
||||
this->publish_state(state.c_str(), state.size());
|
||||
}
|
||||
|
||||
void HomeassistantText::min_retrieved_(StringRef min) {
|
||||
auto min_value = parse_number<int>(min.c_str());
|
||||
if (!min_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_, min.c_str());
|
||||
return;
|
||||
optional<int> HomeassistantText::parse_length_(const char *name, StringRef value) {
|
||||
auto length = parse_number<int>(value.c_str());
|
||||
if (!length.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert '%s' value '%s' to number!", this->entity_id_, name, value.c_str());
|
||||
return nullopt;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Min retrieved: %s", this->entity_id_, min.c_str());
|
||||
this->traits.set_min_length(min_value.value());
|
||||
}
|
||||
|
||||
void HomeassistantText::max_retrieved_(StringRef max) {
|
||||
auto max_value = parse_number<int>(max.c_str());
|
||||
if (!max_value.has_value()) {
|
||||
ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_, max.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Max retrieved: %s", this->entity_id_, max.c_str());
|
||||
this->traits.set_max_length(max_value.value());
|
||||
ESP_LOGD(TAG, "'%s': Retrieved '%s': %s", this->entity_id_, name, value.c_str());
|
||||
return length;
|
||||
}
|
||||
|
||||
void HomeassistantText::mode_retrieved_(StringRef mode) {
|
||||
if (mode == "text") {
|
||||
this->traits.set_mode(text::TEXT_MODE_TEXT);
|
||||
} else if (mode == "password") {
|
||||
this->traits.set_mode(text::TEXT_MODE_PASSWORD);
|
||||
} else {
|
||||
const int16_t index = progmem_string_table_find<TextModeNames>(mode.c_str(), mode.size());
|
||||
if (index < 0) {
|
||||
ESP_LOGW(TAG, "'%s': Unknown 'mode' value '%s'", this->entity_id_, mode.c_str());
|
||||
return;
|
||||
}
|
||||
this->traits.set_mode(static_cast<text::TextMode>(index));
|
||||
ESP_LOGD(TAG, "'%s': Mode retrieved: %s", this->entity_id_, mode.c_str());
|
||||
}
|
||||
|
||||
@@ -57,10 +48,14 @@ void HomeassistantText::setup() {
|
||||
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr,
|
||||
[this](StringRef state) { this->state_changed_(state); });
|
||||
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "min",
|
||||
[this](StringRef min) { this->min_retrieved_(min); });
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "max",
|
||||
[this](StringRef max) { this->max_retrieved_(max); });
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "min", [this](StringRef value) {
|
||||
if (auto min = this->parse_length_("min", value))
|
||||
this->traits.set_min_length(*min);
|
||||
});
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "max", [this](StringRef value) {
|
||||
if (auto max = this->parse_length_("max", value))
|
||||
this->traits.set_max_length(*max);
|
||||
});
|
||||
api::global_api_server->get_home_assistant_state(this->entity_id_, "mode",
|
||||
[this](StringRef mode) { this->mode_retrieved_(mode); });
|
||||
// The "pattern" attribute is not fetched: Home Assistant sends "None" when it is unset,
|
||||
@@ -75,34 +70,20 @@ void HomeassistantText::dump_config() {
|
||||
float HomeassistantText::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
void HomeassistantText::control(const std::string &value) {
|
||||
if (!api::global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "No clients connected to API server");
|
||||
if (!can_send_action(TAG, this->entity_id_, this))
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_API_WIZARD_LINKED_INPUTS
|
||||
if (this->entity_id_[0] == '\0') {
|
||||
ESP_LOGW(TAG, "'%s': No entity ID set yet", LOG_STR_ARG(this->get_log_name()));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// In flash on the ESP8266 and encoded from there
|
||||
static const char SERVICE_TEXT[] PROGMEM = "text.set_value";
|
||||
static const char SERVICE_INPUT_TEXT[] PROGMEM = "input_text.set_value";
|
||||
static const char ENTITY_ID_KEY[] PROGMEM = "entity_id";
|
||||
static const char VALUE_KEY[] PROGMEM = "value";
|
||||
static constexpr char SERVICE_TEXT[] PROGMEM = "text.set_value";
|
||||
static constexpr char SERVICE_INPUT_TEXT[] PROGMEM = "input_text.set_value";
|
||||
static constexpr char VALUE_KEY[] PROGMEM = "value";
|
||||
|
||||
api::HomeassistantActionRequest resp;
|
||||
// An input helper entity id starts with the "input_" its action name starts with
|
||||
const bool input_helper = ESPHOME_strncmp_P(this->entity_id_, SERVICE_INPUT_TEXT, sizeof("input_") - 1) == 0;
|
||||
resp.service =
|
||||
input_helper ? ProgmemStringRef::from_lit(SERVICE_INPUT_TEXT) : ProgmemStringRef::from_lit(SERVICE_TEXT);
|
||||
resp.service = is_input_helper(this->entity_id_, SERVICE_INPUT_TEXT) ? ProgmemStringRef::from_lit(SERVICE_INPUT_TEXT)
|
||||
: ProgmemStringRef::from_lit(SERVICE_TEXT);
|
||||
|
||||
resp.data.init(2);
|
||||
auto &entity_id_kv = resp.data.emplace_back();
|
||||
entity_id_kv.key = ProgmemStringRef::from_lit(ENTITY_ID_KEY);
|
||||
entity_id_kv.value = ProgmemStringRef(StringRef(this->entity_id_));
|
||||
add_entity_id(resp, this->entity_id_);
|
||||
|
||||
auto &value_kv = resp.data.emplace_back();
|
||||
value_kv.key = ProgmemStringRef::from_lit(VALUE_KEY);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/components/text/text.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::homeassistant {
|
||||
@@ -16,8 +17,7 @@ class HomeassistantText final : public text::Text, public Component {
|
||||
|
||||
protected:
|
||||
void state_changed_(StringRef state);
|
||||
void min_retrieved_(StringRef min);
|
||||
void max_retrieved_(StringRef max);
|
||||
optional<int> parse_length_(const char *name, StringRef value);
|
||||
void mode_retrieved_(StringRef mode);
|
||||
|
||||
void control(const std::string &value) override;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
|
||||
api:
|
||||
|
||||
switch:
|
||||
- platform: homeassistant
|
||||
entity_id: switch.porch
|
||||
id: ha_switch
|
||||
@@ -20,6 +20,7 @@ def test_number_and_input_number_entities(
|
||||
|
||||
assert 'ha_number->set_entity_id("number.some_number");' in main_cpp
|
||||
assert 'ha_input_number->set_entity_id("input_number.some_input");' in main_cpp
|
||||
assert any(d.name == "USE_API_HOMEASSISTANT_SERVICES" for d in CORE.defines)
|
||||
|
||||
|
||||
def test_number_rejects_unsupported_domain(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Tests for the homeassistant switch platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def test_switch_alone_enables_actions(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""A switch with no other homeassistant platform still enables the actions it sends."""
|
||||
main_cpp = generate_main(DIR / "switch_valid.yaml")
|
||||
|
||||
assert 'ha_switch->set_entity_id("switch.porch");' in main_cpp
|
||||
assert any(d.name == "USE_API_HOMEASSISTANT_SERVICES" for d in CORE.defines)
|
||||
@@ -16,6 +16,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
# Linked wizard inputs only exist next to homeassistant entities, which need this
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
# homeassistant's shared action check is compiled only with this
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
# Fewer than test_home_assistant_states.cpp registers, so some go past it
|
||||
cg.add_define("HOMEASSISTANT_STATE_SUBSCRIPTION_COUNT", 4)
|
||||
# test_wizard.cpp supplies the tables that codegen emits for a real build;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# test_can_send_action.cpp builds against the API server
|
||||
manifest.dependencies = manifest.dependencies + ["api"]
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifdef USE_HOST
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "esphome/components/homeassistant/homeassistant.h"
|
||||
#include "esphome/components/logger/logger.h"
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
namespace esphome::homeassistant::testing {
|
||||
|
||||
TEST(HomeassistantCanSendActionTest, NoClientConnectedRefusesAndLogs) {
|
||||
// APIServer() makes itself the global_api_server; no connection is ever added
|
||||
auto server = std::make_unique<api::APIServer>();
|
||||
const auto previous_baud_rate = logger::global_logger->get_baud_rate();
|
||||
::testing::internal::CaptureStdout();
|
||||
logger::global_logger->set_baud_rate(115200);
|
||||
const bool allowed = can_send_action("test", "switch.kitchen", nullptr);
|
||||
logger::global_logger->set_baud_rate(previous_baud_rate);
|
||||
const std::string logged = ::testing::internal::GetCapturedStdout();
|
||||
server.reset();
|
||||
api::global_api_server = nullptr;
|
||||
|
||||
EXPECT_FALSE(allowed);
|
||||
EXPECT_NE(logged.find("'switch.kitchen': No clients connected to API server"), std::string::npos)
|
||||
<< "logged: " << logged;
|
||||
}
|
||||
|
||||
} // namespace esphome::homeassistant::testing
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#endif // USE_HOST
|
||||
Reference in new issue
Block a user