Compare commits

...
23 changed files with 246 additions and 228 deletions

No files matched your search

+13 -9
View File
@@ -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)
+2
View File
@@ -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