Add device class support to text sensor (#6202)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
dougiteixeira 2024-02-25 19:29:39 -03:00 committed by GitHub
parent a8ab745479
commit 323849c821
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 133 additions and 1 deletions

View file

@ -600,6 +600,7 @@ message ListEntitiesTextSensorResponse {
string icon = 5; string icon = 5;
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8;
} }
message TextSensorStateResponse { message TextSensorStateResponse {
option (id) = 27; option (id) = 27;

View file

@ -543,6 +543,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor)
msg.icon = text_sensor->get_icon(); msg.icon = text_sensor->get_icon();
msg.disabled_by_default = text_sensor->is_disabled_by_default(); msg.disabled_by_default = text_sensor->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(text_sensor->get_entity_category()); msg.entity_category = static_cast<enums::EntityCategory>(text_sensor->get_entity_category());
msg.device_class = text_sensor->get_device_class();
return this->send_list_entities_text_sensor_response(msg); return this->send_list_entities_text_sensor_response(msg);
} }
#endif #endif

View file

@ -2721,6 +2721,10 @@ bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengt
this->icon = value.as_string(); this->icon = value.as_string();
return true; return true;
} }
case 8: {
this->device_class = value.as_string();
return true;
}
default: default:
return false; return false;
} }
@ -2743,6 +2747,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(5, this->icon); buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default); buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_string(8, this->device_class);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
@ -2776,6 +2781,10 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
out.append(" entity_category: "); out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n"); out.append("\n");
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif

View file

@ -713,6 +713,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage {
std::string icon{}; std::string icon{};
bool disabled_by_default{false}; bool disabled_by_default{false};
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;

View file

@ -1,6 +1,8 @@
#include "mqtt_text_sensor.h" #include "mqtt_text_sensor.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "mqtt_const.h"
#ifdef USE_MQTT #ifdef USE_MQTT
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
@ -13,6 +15,8 @@ using namespace esphome::text_sensor;
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
config.command_topic = false; config.command_topic = false;
} }
void MQTTTextSensor::setup() { void MQTTTextSensor::setup() {

View file

@ -3,6 +3,7 @@ import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import mqtt from esphome.components import mqtt
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
CONF_FILTERS, CONF_FILTERS,
CONF_ICON, CONF_ICON,
@ -14,12 +15,21 @@ from esphome.const import (
CONF_STATE, CONF_STATE,
CONF_FROM, CONF_FROM,
CONF_TO, CONF_TO,
DEVICE_CLASS_DATE,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_TIMESTAMP,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
from esphome.util import Registry from esphome.util import Registry
DEVICE_CLASSES = [
DEVICE_CLASS_DATE,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_TIMESTAMP,
]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@ -112,10 +122,13 @@ async def map_filter_to_code(config, filter_id):
) )
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor),
cv.GenerateID(): cv.declare_id(TextSensor), cv.GenerateID(): cv.declare_id(TextSensor),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(CONF_ON_VALUE): automation.validate_automation( cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{ {
@ -140,12 +153,21 @@ def text_sensor_schema(
*, *,
icon: str = _UNDEF, icon: str = _UNDEF,
entity_category: str = _UNDEF, entity_category: str = _UNDEF,
device_class: str = _UNDEF,
) -> cv.Schema: ) -> cv.Schema:
schema = TEXT_SENSOR_SCHEMA schema = TEXT_SENSOR_SCHEMA
if class_ is not _UNDEF: if class_ is not _UNDEF:
schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)})
if icon is not _UNDEF: if icon is not _UNDEF:
schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon})
if device_class is not _UNDEF:
schema = schema.extend(
{
cv.Optional(
CONF_DEVICE_CLASS, default=device_class
): validate_device_class
}
)
if entity_category is not _UNDEF: if entity_category is not _UNDEF:
schema = schema.extend( schema = schema.extend(
{ {
@ -164,6 +186,9 @@ async def build_filters(config):
async def setup_text_sensor_core_(var, config): async def setup_text_sensor_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config)
if CONF_DEVICE_CLASS in config:
cg.add(var.set_device_class(config[CONF_DEVICE_CLASS]))
if config.get(CONF_FILTERS): # must exist and not be empty if config.get(CONF_FILTERS): # must exist and not be empty
filters = await build_filters(config[CONF_FILTERS]) filters = await build_filters(config[CONF_FILTERS])
cg.add(var.set_filters(filters)) cg.add(var.set_filters(filters))

View file

@ -13,6 +13,9 @@ namespace text_sensor {
#define LOG_TEXT_SENSOR(prefix, type, obj) \ #define LOG_TEXT_SENSOR(prefix, type, obj) \
if ((obj) != nullptr) { \ if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (!(obj)->get_device_class().empty()) { \
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \
} \
if (!(obj)->get_icon().empty()) { \ if (!(obj)->get_icon().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
} \ } \
@ -28,7 +31,7 @@ namespace text_sensor {
public: \ public: \
void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; }
class TextSensor : public EntityBase { class TextSensor : public EntityBase, public EntityBase_DeviceClass {
public: public:
/// Getter-syntax for .state. /// Getter-syntax for .state.
std::string get_state() const; std::string get_state() const;

View file

@ -0,0 +1,58 @@
"""Tests for the text sensor component."""
def test_text_sensor_is_setup(generate_main):
"""
When the text is set in the yaml file, it should be registered in main
"""
# Given
# When
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then
assert "new template_::TemplateTextSensor();" in main_cpp
assert "App.register_text_sensor" in main_cpp
def test_text_sensor_sets_mandatory_fields(generate_main):
"""
When the mandatory fields are set in the yaml, they should be set in main
"""
# Given
# When
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then
assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp
assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp
assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp
def test_text_sensor_config_value_internal_set(generate_main):
"""
Test that the "internal" config value is correctly set
"""
# Given
# When
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then
assert "ts_2->set_internal(true);" in main_cpp
assert "ts_3->set_internal(false);" in main_cpp
def test_text_sensor_device_class_set(generate_main):
"""
When the device_class of text_sensor is set in the yaml file, it should be registered in main
"""
# Given
# When
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then
assert 'ts_2->set_device_class("timestamp");' in main_cpp
assert 'ts_3->set_device_class("date");' in main_cpp

View file

@ -0,0 +1,26 @@
---
esphome:
name: test
platform: ESP8266
board: d1_mini_lite
text_sensor:
- platform: template
id: ts_1
name: "Template Text Sensor 1"
lambda: |-
return {"Hello World"};
- platform: template
id: ts_2
name: "Template Text Sensor 2"
lambda: |-
return {"2023-06-22T18:43:52+00:00"};
device_class: timestamp
internal: true
- platform: template
id: ts_3
name: "Template Text Sensor 3"
lambda: |-
return {"2023-06-22T18:43:52+00:00"};
device_class: date
internal: false

View file

@ -3923,6 +3923,10 @@ text_sensor:
- platform: template - platform: template
name: Template Text Sensor name: Template Text Sensor
id: ${textname}_text id: ${textname}_text
- platform: template
name: Template Text Sensor Timestamp
id: ${textname}_text_timestamp
device_class: timestamp
- platform: wifi_info - platform: wifi_info
scan_results: scan_results:
name: Scan Results name: Scan Results