Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
Daniël Koek 2024-05-30 16:53:19 +01:00
commit c03c7310e4
50 changed files with 2470 additions and 520 deletions

View file

@ -211,6 +211,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @sjtrny esphome/components/ltr390/* @sjtrny
esphome/components/ltr_als_ps/* @latonita
esphome/components/matrix_keypad/* @ssieb esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283 esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger esphome/components/max44009/* @berfenger

View file

@ -1,5 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import web_server
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -8,6 +9,7 @@ from esphome.const import (
CONF_ON_STATE, CONF_ON_STATE,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_CODE, CONF_CODE,
CONF_WEB_SERVER_ID,
) )
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
@ -76,6 +78,8 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
) )
ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
web_server.WEBSERVER_SORTING_SCHEMA
).extend(
{ {
cv.GenerateID(): cv.declare_id(AlarmControlPanel), cv.GenerateID(): cv.declare_id(AlarmControlPanel),
cv.Optional(CONF_ON_STATE): automation.validate_automation( cv.Optional(CONF_ON_STATE): automation.validate_automation(
@ -185,6 +189,9 @@ async def setup_alarm_control_panel_core_(var, config):
for conf in config.get(CONF_ON_READY, []): for conf in config.get(CONF_ON_READY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_alarm_control_panel(var, config): async def register_alarm_control_panel(var, config):

View file

@ -1517,6 +1517,25 @@ message VoiceAssistantAudio {
bool end = 2; bool end = 2;
} }
enum VoiceAssistantTimerEvent {
VOICE_ASSISTANT_TIMER_STARTED = 0;
VOICE_ASSISTANT_TIMER_UPDATED = 1;
VOICE_ASSISTANT_TIMER_CANCELLED = 2;
VOICE_ASSISTANT_TIMER_FINISHED = 3;
}
message VoiceAssistantTimerEventResponse {
option (id) = 115;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
VoiceAssistantTimerEvent event_type = 1;
string timer_id = 2;
string name = 3;
uint32 total_seconds = 4;
uint32 seconds_left = 5;
bool is_active = 6;
}
// ==================== ALARM CONTROL PANEL ==================== // ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState { enum AlarmControlPanelState {

View file

@ -1193,6 +1193,15 @@ void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
voice_assistant::global_voice_assistant->on_audio(msg); voice_assistant::global_voice_assistant->on_audio(msg);
} }
}; };
void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_timer_event(msg);
}
};
#endif #endif

View file

@ -150,6 +150,7 @@ class APIConnection : public APIServerConnection {
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL

View file

@ -475,6 +475,22 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
} }
#endif #endif
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
template<> const char *proto_enum_to_string<enums::VoiceAssistantTimerEvent>(enums::VoiceAssistantTimerEvent value) {
switch (value) {
case enums::VOICE_ASSISTANT_TIMER_STARTED:
return "VOICE_ASSISTANT_TIMER_STARTED";
case enums::VOICE_ASSISTANT_TIMER_UPDATED:
return "VOICE_ASSISTANT_TIMER_UPDATED";
case enums::VOICE_ASSISTANT_TIMER_CANCELLED:
return "VOICE_ASSISTANT_TIMER_CANCELLED";
case enums::VOICE_ASSISTANT_TIMER_FINISHED:
return "VOICE_ASSISTANT_TIMER_FINISHED";
default:
return "UNKNOWN";
}
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<> const char *proto_enum_to_string<enums::AlarmControlPanelState>(enums::AlarmControlPanelState value) { template<> const char *proto_enum_to_string<enums::AlarmControlPanelState>(enums::AlarmControlPanelState value) {
switch (value) { switch (value) {
case enums::ALARM_STATE_DISARMED: case enums::ALARM_STATE_DISARMED:
@ -6857,6 +6873,82 @@ void VoiceAssistantAudio::dump_to(std::string &out) const {
out.append("}"); out.append("}");
} }
#endif #endif
bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->event_type = value.as_enum<enums::VoiceAssistantTimerEvent>();
return true;
}
case 4: {
this->total_seconds = value.as_uint32();
return true;
}
case 5: {
this->seconds_left = value.as_uint32();
return true;
}
case 6: {
this->is_active = value.as_bool();
return true;
}
default:
return false;
}
}
bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->timer_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
default:
return false;
}
}
void VoiceAssistantTimerEventResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::VoiceAssistantTimerEvent>(1, this->event_type);
buffer.encode_string(2, this->timer_id);
buffer.encode_string(3, this->name);
buffer.encode_uint32(4, this->total_seconds);
buffer.encode_uint32(5, this->seconds_left);
buffer.encode_bool(6, this->is_active);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantTimerEventResponse {\n");
out.append(" event_type: ");
out.append(proto_enum_to_string<enums::VoiceAssistantTimerEvent>(this->event_type));
out.append("\n");
out.append(" timer_id: ");
out.append("'").append(this->timer_id).append("'");
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" total_seconds: ");
sprintf(buffer, "%" PRIu32, this->total_seconds);
out.append(buffer);
out.append("\n");
out.append(" seconds_left: ");
sprintf(buffer, "%" PRIu32, this->seconds_left);
out.append(buffer);
out.append("\n");
out.append(" is_active: ");
out.append(YESNO(this->is_active));
out.append("\n");
out.append("}");
}
#endif
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) { switch (field_id) {
case 6: { case 6: {

View file

@ -191,6 +191,12 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_TTS_STREAM_START = 98, VOICE_ASSISTANT_TTS_STREAM_START = 98,
VOICE_ASSISTANT_TTS_STREAM_END = 99, VOICE_ASSISTANT_TTS_STREAM_END = 99,
}; };
enum VoiceAssistantTimerEvent : uint32_t {
VOICE_ASSISTANT_TIMER_STARTED = 0,
VOICE_ASSISTANT_TIMER_UPDATED = 1,
VOICE_ASSISTANT_TIMER_CANCELLED = 2,
VOICE_ASSISTANT_TIMER_FINISHED = 3,
};
enum AlarmControlPanelState : uint32_t { enum AlarmControlPanelState : uint32_t {
ALARM_STATE_DISARMED = 0, ALARM_STATE_DISARMED = 0,
ALARM_STATE_ARMED_HOME = 1, ALARM_STATE_ARMED_HOME = 1,
@ -1775,6 +1781,23 @@ class VoiceAssistantAudio : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class VoiceAssistantTimerEventResponse : public ProtoMessage {
public:
enums::VoiceAssistantTimerEvent event_type{};
std::string timer_id{};
std::string name{};
uint32_t total_seconds{0};
uint32_t seconds_left{0};
bool is_active{false};
void encode(ProtoWriteBuffer buffer) const override;
#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;
};
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
public: public:
std::string object_id{}; std::string object_id{};

View file

@ -484,6 +484,8 @@ bool APIServerConnectionBase::send_voice_assistant_audio(const VoiceAssistantAud
return this->send_message_<VoiceAssistantAudio>(msg, 106); return this->send_message_<VoiceAssistantAudio>(msg, 106);
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response( bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
const ListEntitiesAlarmControlPanelResponse &msg) { const ListEntitiesAlarmControlPanelResponse &msg) {
@ -1093,6 +1095,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str());
#endif #endif
this->on_date_time_command_request(msg); this->on_date_time_command_request(msg);
#endif
break;
}
case 115: {
#ifdef USE_VOICE_ASSISTANT
VoiceAssistantTimerEventResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_timer_event_response(msg);
#endif #endif
break; break;
} }

View file

@ -244,6 +244,9 @@ class APIServerConnectionBase : public ProtoService {
bool send_voice_assistant_audio(const VoiceAssistantAudio &msg); bool send_voice_assistant_audio(const VoiceAssistantAudio &msg);
virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){};
#endif #endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){};
#endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg); bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
#endif #endif

View file

@ -4,7 +4,7 @@ from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
from esphome import automation, core from esphome import automation, core
from esphome.automation import Condition, maybe_simple_id from esphome.automation import Condition, maybe_simple_id
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_DELAY, CONF_DELAY,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
@ -27,6 +27,7 @@ from esphome.const import (
CONF_TIMING, CONF_TIMING,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY_CHARGING,
DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_MONOXIDE,
@ -385,7 +386,10 @@ def validate_click_timing(value):
return value return value
BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(BinarySensor), cv.GenerateID(): cv.declare_id(BinarySensor),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
@ -421,7 +425,9 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex
cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All( cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All(
automation.validate_automation( automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DoubleClickTrigger
),
cv.Optional( cv.Optional(
CONF_MIN_LENGTH, default="50ms" CONF_MIN_LENGTH, default="50ms"
): cv.positive_time_period_milliseconds, ): cv.positive_time_period_milliseconds,
@ -449,6 +455,7 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex
} }
), ),
} }
)
) )
_UNDEF = object() _UNDEF = object()
@ -536,6 +543,10 @@ async def setup_binary_sensor_core_(var, config):
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_binary_sensor(var, config): async def register_binary_sensor(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@ -11,6 +11,7 @@ from esphome.const import (
CONF_ON_PRESS, CONF_ON_PRESS,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
DEVICE_CLASS_EMPTY, DEVICE_CLASS_EMPTY,
DEVICE_CLASS_IDENTIFY, DEVICE_CLASS_IDENTIFY,
DEVICE_CLASS_RESTART, DEVICE_CLASS_RESTART,
@ -43,7 +44,10 @@ ButtonPressTrigger = button_ns.class_(
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
BUTTON_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( BUTTON_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
@ -53,6 +57,7 @@ BUTTON_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e
} }
), ),
} }
)
) )
_UNDEF = object() _UNDEF = object()
@ -92,6 +97,10 @@ async def setup_button_core_(var, config):
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_button(var, config): async def register_button(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
from esphome import automation from esphome import automation
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_ACTION_STATE_TOPIC, CONF_ACTION_STATE_TOPIC,
CONF_AWAY, CONF_AWAY,
@ -44,6 +44,7 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VISUAL, CONF_VISUAL,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -150,7 +151,10 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any(
), ),
) )
CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( CLIMATE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Climate), cv.GenerateID(): cv.declare_id(Climate),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
@ -237,6 +241,7 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
} }
), ),
} }
)
) )
@ -403,6 +408,10 @@ async def setup_climate_core_(var, config):
trigger, [(ClimateCall.operator("ref"), "x")], conf trigger, [(ClimateCall.operator("ref"), "x")], conf
) )
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_climate(var, config): async def register_climate(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id, Condition from esphome.automation import maybe_simple_id, Condition
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
@ -16,6 +16,7 @@ from esphome.const import (
CONF_TILT_STATE_TOPIC, CONF_TILT_STATE_TOPIC,
CONF_STOP, CONF_STOP,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
DEVICE_CLASS_AWNING, DEVICE_CLASS_AWNING,
DEVICE_CLASS_BLIND, DEVICE_CLASS_BLIND,
@ -88,7 +89,10 @@ CoverClosedTrigger = cover_ns.class_(
CONF_ON_CLOSED = "on_closed" CONF_ON_CLOSED = "on_closed"
COVER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Cover), cv.GenerateID(): cv.declare_id(Cover),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
@ -116,6 +120,7 @@ COVER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).ex
} }
), ),
} }
)
) )
@ -132,6 +137,10 @@ async def setup_cover_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import mqtt, time from esphome.components import mqtt, web_server, time
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_ON_TIME, CONF_ON_TIME,
@ -11,6 +11,7 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE, CONF_TYPE,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_DATE, CONF_DATE,
CONF_DATETIME, CONF_DATETIME,
CONF_TIME, CONF_TIME,
@ -63,7 +64,10 @@ DATETIME_MODES = [
] ]
_DATETIME_SCHEMA = cv.Schema( _DATETIME_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.Optional(CONF_ON_VALUE): automation.validate_automation( cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{ {
@ -72,7 +76,8 @@ _DATETIME_SCHEMA = cv.Schema(
), ),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
} }
).extend(cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)) )
)
def date_schema(class_: MockObjClass) -> cv.Schema: def date_schema(class_: MockObjClass) -> cv.Schema:
@ -128,6 +133,9 @@ async def setup_datetime_core_(var, config):
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
for conf in config.get(CONF_ON_VALUE, []): for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf) await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf)

View file

@ -86,9 +86,14 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
if (this->model_ == DHT_MODEL_DHT11) { if (this->model_ == DHT_MODEL_DHT11) {
delayMicroseconds(18000); delayMicroseconds(18000);
} else if (this->model_ == DHT_MODEL_SI7021) { } else if (this->model_ == DHT_MODEL_SI7021) {
#ifdef USE_ESP8266
delayMicroseconds(500); delayMicroseconds(500);
this->pin_->digital_write(true); this->pin_->digital_write(true);
delayMicroseconds(40); delayMicroseconds(40);
#else
delayMicroseconds(400);
this->pin_->digital_write(true);
#endif
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000); delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {

View file

@ -2,10 +2,11 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_OSCILLATING, CONF_OSCILLATING,
CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_COMMAND_TOPIC,
CONF_OSCILLATION_STATE_TOPIC, CONF_OSCILLATION_STATE_TOPIC,
@ -79,7 +80,10 @@ FanPresetSetTrigger = fan_ns.class_(
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( FAN_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Fan), cv.GenerateID(): cv.declare_id(Fan),
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
@ -121,12 +125,16 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte
), ),
cv.Optional(CONF_ON_DIRECTION_SET): automation.validate_automation( cv.Optional(CONF_ON_DIRECTION_SET): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanDirectionSetTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FanDirectionSetTrigger
),
} }
), ),
cv.Optional(CONF_ON_OSCILLATING_SET): automation.validate_automation( cv.Optional(CONF_ON_OSCILLATING_SET): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanOscillatingSetTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FanOscillatingSetTrigger
),
} }
), ),
cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation(
@ -140,6 +148,7 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte
} }
), ),
} }
)
) )
_PRESET_MODES_SCHEMA = cv.All( _PRESET_MODES_SCHEMA = cv.All(
@ -209,6 +218,10 @@ async def setup_fan_core_(var, config):
if (speed_command_topic := config.get(CONF_SPEED_COMMAND_TOPIC)) is not None: if (speed_command_topic := config.get(CONF_SPEED_COMMAND_TOPIC)) is not None:
cg.add(mqtt_.set_custom_speed_command_topic(speed_command_topic)) cg.add(mqtt_.set_custom_speed_command_topic(speed_command_topic))
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
for conf in config.get(CONF_ON_STATE, []): for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(Fan.operator("ptr"), "x")], conf) await automation.build_automation(trigger, [(Fan.operator("ptr"), "x")], conf)

View file

@ -19,10 +19,27 @@ void I2SAudioSpeaker::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker..."); ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker...");
this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent)); this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent));
if (this->buffer_queue_ == nullptr) {
ESP_LOGE(TAG, "Failed to create buffer queue");
this->mark_failed();
return;
}
this->event_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(TaskEvent)); this->event_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(TaskEvent));
if (this->event_queue_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event queue");
this->mark_failed();
return;
}
} }
void I2SAudioSpeaker::start() { this->state_ = speaker::STATE_STARTING; } void I2SAudioSpeaker::start() {
if (this->is_failed()) {
ESP_LOGE(TAG, "Cannot start audio, speaker failed to setup");
return;
}
this->state_ = speaker::STATE_STARTING;
}
void I2SAudioSpeaker::start_() { void I2SAudioSpeaker::start_() {
if (!this->parent_->try_lock()) { if (!this->parent_->try_lock()) {
return; // Waiting for another i2s component to return lock return; // Waiting for another i2s component to return lock
@ -141,6 +158,8 @@ void I2SAudioSpeaker::player_task(void *params) {
} }
void I2SAudioSpeaker::stop() { void I2SAudioSpeaker::stop() {
if (this->is_failed())
return;
if (this->state_ == speaker::STATE_STOPPED) if (this->state_ == speaker::STATE_STOPPED)
return; return;
if (this->state_ == speaker::STATE_STARTING) { if (this->state_ == speaker::STATE_STARTING) {
@ -200,6 +219,10 @@ void I2SAudioSpeaker::loop() {
} }
size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length) { size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length) {
if (this->is_failed()) {
ESP_LOGE(TAG, "Cannot play audio, speaker failed to setup");
return 0;
}
if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) {
this->start(); this->start();
} }

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.automation as auto import esphome.automation as auto
from esphome.components import mqtt, power_supply from esphome.components import mqtt, power_supply, web_server
from esphome.const import ( from esphome.const import (
CONF_COLOR_CORRECT, CONF_COLOR_CORRECT,
CONF_DEFAULT_TRANSITION_LENGTH, CONF_DEFAULT_TRANSITION_LENGTH,
@ -10,6 +10,7 @@ from esphome.const import (
CONF_GAMMA_CORRECT, CONF_GAMMA_CORRECT,
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_POWER_SUPPLY, CONF_POWER_SUPPLY,
CONF_RESTORE_MODE, CONF_RESTORE_MODE,
CONF_ON_TURN_OFF, CONF_ON_TURN_OFF,
@ -56,10 +57,15 @@ RESTORE_MODES = {
"RESTORE_AND_ON": LightRestoreMode.LIGHT_RESTORE_AND_ON, "RESTORE_AND_ON": LightRestoreMode.LIGHT_RESTORE_AND_ON,
} }
LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( LIGHT_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(LightState), cv.GenerateID(): cv.declare_id(LightState),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTJSONLightComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTJSONLightComponent
),
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
RESTORE_MODES, upper=True, space="_" RESTORE_MODES, upper=True, space="_"
), ),
@ -79,6 +85,7 @@ LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).ex
} }
), ),
} }
)
) )
BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
@ -173,6 +180,10 @@ async def setup_light_core_(light_var, output_var, config):
mqtt_ = cg.new_Pvariable(mqtt_id, light_var) mqtt_ = cg.new_Pvariable(mqtt_id, light_var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, light_var, config)
async def register_light(output_var, config): async def register_light(output_var, config):
light_var = cg.new_Pvariable(config[CONF_ID], output_var) light_var = cg.new_Pvariable(config[CONF_ID], output_var)

View file

@ -2,13 +2,14 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import Condition, maybe_simple_id from esphome.automation import Condition, maybe_simple_id
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_ON_LOCK, CONF_ON_LOCK,
CONF_ON_UNLOCK, CONF_ON_UNLOCK,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
@ -30,7 +31,10 @@ LockCondition = lock_ns.class_("LockCondition", Condition)
LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template()) LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template())
LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template()) LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template())
LOCK_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( LOCK_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent),
cv.Optional(CONF_ON_LOCK): automation.validate_automation( cv.Optional(CONF_ON_LOCK): automation.validate_automation(
@ -44,6 +48,7 @@ LOCK_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).ext
} }
), ),
} }
)
) )
@ -61,6 +66,10 @@ async def setup_lock_core_(var, config):
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_lock(var, config): async def register_lock(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -2,14 +2,15 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_AMBIENT_LIGHT,
CONF_GAIN, CONF_GAIN,
CONF_ID,
CONF_LIGHT, CONF_LIGHT,
CONF_RESOLUTION, CONF_RESOLUTION,
UNIT_LUX,
ICON_BRIGHTNESS_5,
DEVICE_CLASS_EMPTY, DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
ICON_BRIGHTNESS_5,
UNIT_LUX,
) )
CODEOWNERS = ["@sjtrny"] CODEOWNERS = ["@sjtrny"]
@ -21,7 +22,6 @@ LTR390Component = ltr390_ns.class_(
"LTR390Component", cg.PollingComponent, i2c.I2CDevice "LTR390Component", cg.PollingComponent, i2c.I2CDevice
) )
CONF_AMBIENT_LIGHT = "ambient_light"
CONF_UV_INDEX = "uv_index" CONF_UV_INDEX = "uv_index"
CONF_UV = "uv" CONF_UV = "uv"
CONF_WINDOW_CORRECTION_FACTOR = "window_correction_factor" CONF_WINDOW_CORRECTION_FACTOR = "window_correction_factor"

View file

@ -0,0 +1 @@
CODEOWNERS = ["@latonita"]

View file

@ -0,0 +1,519 @@
#include "ltr_als_ps.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
using esphome::i2c::ErrorCode;
namespace esphome {
namespace ltr_als_ps {
static const char *const TAG = "ltr_als_ps";
static const uint8_t MAX_TRIES = 5;
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
size_t i = 0;
size_t idx = -1;
while (idx == -1 && i < size) {
if (array[i] == val) {
idx = i;
break;
}
i++;
}
if (idx == -1 || i + 1 >= size)
return val;
return array[i + 1];
}
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
size_t i = size - 1;
size_t idx = -1;
while (idx == -1 && i > 0) {
if (array[i] == val) {
idx = i;
break;
}
i--;
}
if (idx == -1 || i == 0)
return val;
return array[i - 1];
}
static uint16_t get_itime_ms(IntegrationTime time) {
static const uint16_t ALS_INT_TIME[8] = {100, 50, 200, 400, 150, 250, 300, 350};
return ALS_INT_TIME[time & 0b111];
}
static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) {
static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000};
return ALS_MEAS_RATE[rate & 0b111];
}
static float get_gain_coeff(AlsGain gain) {
static const float ALS_GAIN[8] = {1, 2, 4, 8, 0, 0, 48, 96};
return ALS_GAIN[gain & 0b111];
}
static float get_ps_gain_coeff(PsGain gain) {
static const float PS_GAIN[4] = {16, 0, 32, 64};
return PS_GAIN[gain & 0b11];
}
void LTRAlsPsComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up LTR-303/329/55x/659");
// As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive
this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; });
}
void LTRAlsPsComponent::dump_config() {
auto get_device_type = [](LtrType typ) {
switch (typ) {
case LtrType::LTR_TYPE_ALS_ONLY:
return "ALS only";
case LtrType::LTR_TYPE_PS_ONLY:
return "PS only";
case LtrType::LTR_TYPE_ALS_AND_PS:
return "ALS + PS";
default:
return "Unknown";
}
};
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Device type: %s", get_device_type(this->ltr_type_));
if (this->is_als_()) {
ESP_LOGCONFIG(TAG, " Automatic mode: %s", ONOFF(this->automatic_mode_enabled_));
ESP_LOGCONFIG(TAG, " Gain: %.0fx", get_gain_coeff(this->gain_));
ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_));
ESP_LOGCONFIG(TAG, " Measurement repeat rate: %d ms", get_meas_time_ms(this->repeat_rate_));
ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_);
LOG_SENSOR(" ", "ALS calculated lux", this->ambient_light_sensor_);
LOG_SENSOR(" ", "CH1 Infrared counts", this->infrared_counts_sensor_);
LOG_SENSOR(" ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_);
LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_);
}
if (this->is_ps_()) {
ESP_LOGCONFIG(TAG, " Proximity gain: %.0fx", get_ps_gain_coeff(this->ps_gain_));
ESP_LOGCONFIG(TAG, " Proximity cooldown time: %d s", this->ps_cooldown_time_s_);
ESP_LOGCONFIG(TAG, " Proximity high threshold: %d", this->ps_threshold_high_);
ESP_LOGCONFIG(TAG, " Proximity low threshold: %d", this->ps_threshold_low_);
LOG_SENSOR(" ", "Proximity counts", this->proximity_counts_sensor_);
}
LOG_UPDATE_INTERVAL(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with I2C LTR-303/329/55x/659 failed!");
}
}
void LTRAlsPsComponent::update() {
ESP_LOGV(TAG, "Updating");
if (this->is_ready() && this->state_ == State::IDLE) {
ESP_LOGV(TAG, "Initiating new data collection");
this->state_ = this->automatic_mode_enabled_ ? State::COLLECTING_DATA_AUTO : State::WAITING_FOR_DATA;
this->als_readings_.ch0 = 0;
this->als_readings_.ch1 = 0;
this->als_readings_.gain = this->gain_;
this->als_readings_.integration_time = this->integration_time_;
this->als_readings_.lux = 0;
this->als_readings_.number_of_adjustments = 0;
} else {
ESP_LOGV(TAG, "Component not ready yet");
}
}
void LTRAlsPsComponent::loop() {
ErrorCode err = i2c::ERROR_OK;
static uint8_t tries{0};
switch (this->state_) {
case State::DELAYED_SETUP:
err = this->write(nullptr, 0);
if (err != i2c::ERROR_OK) {
ESP_LOGV(TAG, "i2c connection failed");
this->mark_failed();
}
this->configure_reset_();
if (this->is_als_()) {
this->configure_als_();
this->configure_integration_time_(this->integration_time_);
}
if (this->is_ps_()) {
this->configure_ps_();
}
this->state_ = State::IDLE;
break;
case State::IDLE:
if (this->is_ps_()) {
check_and_trigger_ps_();
}
break;
case State::WAITING_FOR_DATA:
if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) {
tries = 0;
ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
get_itime_ms(this->als_readings_.integration_time));
this->read_sensor_data_(this->als_readings_);
this->state_ = State::DATA_COLLECTED;
this->apply_lux_calculation_(this->als_readings_);
} else if (tries >= MAX_TRIES) {
ESP_LOGW(TAG, "Can't get data after several tries.");
tries = 0;
this->status_set_warning();
this->state_ = State::IDLE;
return;
} else {
tries++;
}
break;
case State::COLLECTING_DATA_AUTO:
case State::DATA_COLLECTED:
// first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration
if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) {
this->state_ = State::ADJUSTMENT_IN_PROGRESS;
ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
get_itime_ms(this->als_readings_.integration_time));
this->configure_integration_time_(this->als_readings_.integration_time);
this->configure_gain_(this->als_readings_.gain);
// if sensitivity adjustment needed - need to wait for first data samples after setting new parameters
this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_),
[this]() { this->state_ = State::WAITING_FOR_DATA; });
} else {
this->state_ = State::READY_TO_PUBLISH;
}
break;
case State::ADJUSTMENT_IN_PROGRESS:
// nothing to be done, just waiting for the timeout
break;
case State::READY_TO_PUBLISH:
this->publish_data_part_1_(this->als_readings_);
this->state_ = State::KEEP_PUBLISHING;
break;
case State::KEEP_PUBLISHING:
this->publish_data_part_2_(this->als_readings_);
this->status_clear_warning();
this->state_ = State::IDLE;
break;
default:
break;
}
}
void LTRAlsPsComponent::check_and_trigger_ps_() {
static uint32_t last_high_trigger_time{0};
static uint32_t last_low_trigger_time{0};
uint16_t ps_data = this->read_ps_data_();
uint32_t now = millis();
if (ps_data != this->ps_readings_) {
this->ps_readings_ = ps_data;
// Higher values - object is closer to sensor
if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
last_high_trigger_time = now;
ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data,
this->ps_threshold_high_);
this->on_ps_high_trigger_callback_.call();
} else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
last_low_trigger_time = now;
ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data,
this->ps_threshold_low_);
this->on_ps_low_trigger_callback_.call();
}
}
}
bool LTRAlsPsComponent::check_part_number_() {
uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get();
if (manuf_id != 0x05) { // 0x05 is Lite-On Semiconductor Corp. ID
ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id);
this->mark_failed();
return false;
}
// Things getting not really funny here, we can't identify device type by part number ID
// ======================== ========= ===== =================
// Device Part ID Rev Capabilities
// ======================== ========= ===== =================
// Ltr-329/ltr-303 0x0a 0x00 Als 16b
// Ltr-553/ltr-556/ltr-556 0x09 0x02 Als 16b + Ps 11b diff nm sens
// Ltr-659 0x09 0x02 Ps 11b and ps gain
//
// There are other devices which might potentially work with default settings,
// but registers layout is different and we can't use them properly. For ex. ltr-558
PartIdRegister part_id{0};
part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get();
if (part_id.part_number_id != 0x0a && part_id.part_number_id != 0x09) {
ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. It might not work properly.", part_id.part_number_id);
this->status_set_warning();
return true;
}
return true;
}
void LTRAlsPsComponent::configure_reset_() {
ESP_LOGV(TAG, "Resetting");
AlsControlRegister als_ctrl{0};
als_ctrl.sw_reset = true;
this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
delay(2);
uint8_t tries = MAX_TRIES;
do {
ESP_LOGV(TAG, "Waiting for chip to reset");
delay(2);
als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
} while (als_ctrl.sw_reset && tries--); // while sw reset bit is on - keep waiting
if (als_ctrl.sw_reset) {
ESP_LOGW(TAG, "Reset timed out");
}
}
void LTRAlsPsComponent::configure_als_() {
AlsControlRegister als_ctrl{0};
als_ctrl.sw_reset = false;
als_ctrl.active_mode = true;
als_ctrl.gain = this->gain_;
ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw);
this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
delay(5);
uint8_t tries = MAX_TRIES;
do {
ESP_LOGV(TAG, "Waiting for device to become active...");
delay(2);
als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
} while (!als_ctrl.active_mode && tries--); // while active mode is not set - keep waiting
if (!als_ctrl.active_mode) {
ESP_LOGW(TAG, "Failed to activate device");
}
}
void LTRAlsPsComponent::configure_ps_() {
PsMeasurementRateRegister ps_meas{0};
ps_meas.ps_measurement_rate = PsMeasurementRate::PS_MEAS_RATE_50MS;
this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw;
PsControlRegister ps_ctrl{0};
ps_ctrl.ps_mode_active = true;
ps_ctrl.ps_mode_xxx = true;
this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw;
}
uint16_t LTRAlsPsComponent::read_ps_data_() {
AlsPsStatusRegister als_status{0};
als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
if (!als_status.ps_new_data || als_status.data_invalid) {
return this->ps_readings_;
}
uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get();
PsData1Register ps_high;
ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get();
uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low);
if (ps_high.ps_saturation_flag) {
return 0x7ff; // full 11 bit range
}
return val;
}
void LTRAlsPsComponent::configure_gain_(AlsGain gain) {
AlsControlRegister als_ctrl{0};
als_ctrl.active_mode = true;
als_ctrl.gain = gain;
this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
delay(2);
AlsControlRegister read_als_ctrl{0};
read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
if (read_als_ctrl.gain != gain) {
ESP_LOGW(TAG, "Failed to set gain. We will try one more time.");
this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
delay(2);
}
}
void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) {
MeasurementRateRegister meas{0};
meas.measurement_repeat_rate = this->repeat_rate_;
meas.integration_time = time;
this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
delay(2);
MeasurementRateRegister read_meas{0};
read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get();
if (read_meas.integration_time != time) {
ESP_LOGW(TAG, "Failed to set integration time. We will try one more time.");
this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
delay(2);
}
}
DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) {
AlsPsStatusRegister als_status{0};
als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
if (!als_status.als_new_data)
return DataAvail::NO_DATA;
if (als_status.data_invalid) {
ESP_LOGW(TAG, "Data available but not valid");
return DataAvail::BAD_DATA;
}
ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain));
if (data.gain != als_status.gain) {
ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain));
return DataAvail::BAD_DATA;
}
return DataAvail::DATA_OK;
}
void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) {
data.ch1 = 0;
data.ch0 = 0;
uint8_t ch1_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_0).get();
uint8_t ch1_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_1).get();
uint8_t ch0_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_0).get();
uint8_t ch0_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_1).get();
data.ch1 = encode_uint16(ch1_1, ch1_0);
data.ch0 = encode_uint16(ch0_1, ch0_0);
ESP_LOGV(TAG, "Got sensor data: CH1 = %d, CH0 = %d", data.ch1, data.ch0);
}
bool LTRAlsPsComponent::are_adjustments_required_(AlsReadings &data) {
if (!this->automatic_mode_enabled_)
return false;
if (data.number_of_adjustments > 15) {
// sometimes sensors fail to change sensitivity. this prevents us from infinite loop
ESP_LOGW(TAG, "Too many sensitivity adjustments done. Apparently, sensor reconfiguration fails. Stopping.");
return false;
}
data.number_of_adjustments++;
// Recommended thresholds as per datasheet
static const uint16_t LOW_INTENSITY_THRESHOLD = 1000;
static const uint16_t HIGH_INTENSITY_THRESHOLD = 30000;
static const AlsGain GAINS[GAINS_COUNT] = {GAIN_1, GAIN_2, GAIN_4, GAIN_8, GAIN_48, GAIN_96};
static const IntegrationTime INT_TIMES[TIMES_COUNT] = {
INTEGRATION_TIME_50MS, INTEGRATION_TIME_100MS, INTEGRATION_TIME_150MS, INTEGRATION_TIME_200MS,
INTEGRATION_TIME_250MS, INTEGRATION_TIME_300MS, INTEGRATION_TIME_350MS, INTEGRATION_TIME_400MS};
if (data.ch0 <= LOW_INTENSITY_THRESHOLD) {
AlsGain next_gain = get_next(GAINS, data.gain);
if (next_gain != data.gain) {
data.gain = next_gain;
ESP_LOGV(TAG, "Low illuminance. Increasing gain.");
return true;
}
IntegrationTime next_time = get_next(INT_TIMES, data.integration_time);
if (next_time != data.integration_time) {
data.integration_time = next_time;
ESP_LOGV(TAG, "Low illuminance. Increasing integration time.");
return true;
}
} else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD) {
AlsGain prev_gain = get_prev(GAINS, data.gain);
if (prev_gain != data.gain) {
data.gain = prev_gain;
ESP_LOGV(TAG, "High illuminance. Decreasing gain.");
return true;
}
IntegrationTime prev_time = get_prev(INT_TIMES, data.integration_time);
if (prev_time != data.integration_time) {
data.integration_time = prev_time;
ESP_LOGV(TAG, "High illuminance. Decreasing integration time.");
return true;
}
} else {
ESP_LOGD(TAG, "Illuminance is sufficient.");
return false;
}
ESP_LOGD(TAG, "Can't adjust sensitivity anymore.");
return false;
}
void LTRAlsPsComponent::apply_lux_calculation_(AlsReadings &data) {
if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) {
ESP_LOGW(TAG, "Sensors got saturated");
data.lux = 0.0f;
return;
}
if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) {
ESP_LOGW(TAG, "Sensors blacked out");
data.lux = 0.0f;
return;
}
float ch0 = data.ch0;
float ch1 = data.ch1;
float ratio = ch1 / (ch0 + ch1);
float als_gain = get_gain_coeff(data.gain);
float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f;
float inv_pfactor = this->glass_attenuation_factor_;
float lux = 0.0f;
if (ratio < 0.45) {
lux = (1.7743 * ch0 + 1.1059 * ch1);
} else if (ratio < 0.64 && ratio >= 0.45) {
lux = (4.2785 * ch0 - 1.9548 * ch1);
} else if (ratio < 0.85 && ratio >= 0.64) {
lux = (0.5926 * ch0 + 0.1185 * ch1);
} else {
ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
lux = 0.0f;
}
lux = inv_pfactor * lux / als_gain / als_time;
data.lux = lux;
ESP_LOGV(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain,
als_time, inv_pfactor, lux);
}
void LTRAlsPsComponent::publish_data_part_1_(AlsReadings &data) {
if (this->proximity_counts_sensor_ != nullptr) {
this->proximity_counts_sensor_->publish_state(this->ps_readings_);
}
if (this->ambient_light_sensor_ != nullptr) {
this->ambient_light_sensor_->publish_state(data.lux);
}
if (this->infrared_counts_sensor_ != nullptr) {
this->infrared_counts_sensor_->publish_state(data.ch1);
}
if (this->full_spectrum_counts_sensor_ != nullptr) {
this->full_spectrum_counts_sensor_->publish_state(data.ch0);
}
}
void LTRAlsPsComponent::publish_data_part_2_(AlsReadings &data) {
if (this->actual_gain_sensor_ != nullptr) {
this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain));
}
if (this->actual_integration_time_sensor_ != nullptr) {
this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time));
}
}
} // namespace ltr_als_ps
} // namespace esphome

View file

@ -0,0 +1,184 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/optional.h"
#include "esphome/core/automation.h"
#include "ltr_definitions.h"
namespace esphome {
namespace ltr_als_ps {
enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK };
enum LtrType : uint8_t {
LTR_TYPE_UNKNOWN = 0,
LTR_TYPE_ALS_ONLY = 1,
LTR_TYPE_PS_ONLY = 2,
LTR_TYPE_ALS_AND_PS = 3,
};
class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice {
public:
//
// EspHome framework functions
//
float get_setup_priority() const override { return setup_priority::DATA; }
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
// Configuration setters : General
//
void set_ltr_type(LtrType type) { this->ltr_type_ = type; }
// Configuration setters : ALS
//
void set_als_auto_mode(bool enable) { this->automatic_mode_enabled_ = enable; }
void set_als_gain(AlsGain gain) { this->gain_ = gain; }
void set_als_integration_time(IntegrationTime time) { this->integration_time_ = time; }
void set_als_meas_repeat_rate(MeasurementRepeatRate rate) { this->repeat_rate_ = rate; }
void set_als_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; }
// Configuration setters : PS
//
void set_ps_high_threshold(uint16_t threshold) { this->ps_threshold_high_ = threshold; }
void set_ps_low_threshold(uint16_t threshold) { this->ps_threshold_low_ = threshold; }
void set_ps_cooldown_time_s(uint16_t time) { this->ps_cooldown_time_s_ = time; }
void set_ps_gain(PsGain gain) { this->ps_gain_ = gain; }
// Sensors setters
//
void set_ambient_light_sensor(sensor::Sensor *sensor) { this->ambient_light_sensor_ = sensor; }
void set_full_spectrum_counts_sensor(sensor::Sensor *sensor) { this->full_spectrum_counts_sensor_ = sensor; }
void set_infrared_counts_sensor(sensor::Sensor *sensor) { this->infrared_counts_sensor_ = sensor; }
void set_actual_gain_sensor(sensor::Sensor *sensor) { this->actual_gain_sensor_ = sensor; }
void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; }
void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; }
protected:
//
// Internal state machine, used to split all the actions into
// small steps in loop() to make sure we are not blocking execution
//
enum class State : uint8_t {
NOT_INITIALIZED,
DELAYED_SETUP,
IDLE,
WAITING_FOR_DATA,
COLLECTING_DATA_AUTO,
DATA_COLLECTED,
ADJUSTMENT_IN_PROGRESS,
READY_TO_PUBLISH,
KEEP_PUBLISHING
} state_{State::NOT_INITIALIZED};
LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY};
//
// Current measurements data
//
struct AlsReadings {
uint16_t ch0{0};
uint16_t ch1{0};
AlsGain gain{AlsGain::GAIN_1};
IntegrationTime integration_time{IntegrationTime::INTEGRATION_TIME_100MS};
float lux{0.0f};
uint8_t number_of_adjustments{0};
} als_readings_;
uint16_t ps_readings_{0xfffe};
inline bool is_als_() const {
return this->ltr_type_ == LtrType::LTR_TYPE_ALS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS;
}
inline bool is_ps_() const {
return this->ltr_type_ == LtrType::LTR_TYPE_PS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS;
}
//
// Device interaction and data manipulation
//
bool check_part_number_();
void configure_reset_();
void configure_als_();
void configure_integration_time_(IntegrationTime time);
void configure_gain_(AlsGain gain);
DataAvail is_als_data_ready_(AlsReadings &data);
void read_sensor_data_(AlsReadings &data);
bool are_adjustments_required_(AlsReadings &data);
void apply_lux_calculation_(AlsReadings &data);
void publish_data_part_1_(AlsReadings &data);
void publish_data_part_2_(AlsReadings &data);
void configure_ps_();
uint16_t read_ps_data_();
void check_and_trigger_ps_();
//
// Component configuration
//
bool automatic_mode_enabled_{true};
AlsGain gain_{AlsGain::GAIN_1};
IntegrationTime integration_time_{IntegrationTime::INTEGRATION_TIME_100MS};
MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS};
float glass_attenuation_factor_{1.0};
uint16_t ps_cooldown_time_s_{5};
PsGain ps_gain_{PsGain::PS_GAIN_16};
uint16_t ps_threshold_high_{0xffff};
uint16_t ps_threshold_low_{0x0000};
//
// Sensors for publishing data
//
sensor::Sensor *infrared_counts_sensor_{nullptr}; // direct reading CH1, infrared only
sensor::Sensor *full_spectrum_counts_sensor_{nullptr}; // direct reading CH0, infrared + visible light
sensor::Sensor *ambient_light_sensor_{nullptr}; // calculated lux
sensor::Sensor *actual_gain_sensor_{nullptr}; // actual gain of reading
sensor::Sensor *actual_integration_time_sensor_{nullptr}; // actual integration time
sensor::Sensor *proximity_counts_sensor_{nullptr}; // proximity sensor
bool is_any_als_sensor_enabled_() const {
return this->ambient_light_sensor_ != nullptr || this->full_spectrum_counts_sensor_ != nullptr ||
this->infrared_counts_sensor_ != nullptr || this->actual_gain_sensor_ != nullptr ||
this->actual_integration_time_sensor_ != nullptr;
}
bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; }
//
// Trigger section for the automations
//
friend class LTRPsHighTrigger;
friend class LTRPsLowTrigger;
CallbackManager<void()> on_ps_high_trigger_callback_;
CallbackManager<void()> on_ps_low_trigger_callback_;
void add_on_ps_high_trigger_callback_(std::function<void()> callback) {
this->on_ps_high_trigger_callback_.add(std::move(callback));
}
void add_on_ps_low_trigger_callback_(std::function<void()> callback) {
this->on_ps_low_trigger_callback_.add(std::move(callback));
}
};
class LTRPsHighTrigger : public Trigger<> {
public:
explicit LTRPsHighTrigger(LTRAlsPsComponent *parent) {
parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); });
}
};
class LTRPsLowTrigger : public Trigger<> {
public:
explicit LTRPsLowTrigger(LTRAlsPsComponent *parent) {
parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); });
}
};
} // namespace ltr_als_ps
} // namespace esphome

View file

@ -0,0 +1,275 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace ltr_als_ps {
enum class CommandRegisters : uint8_t {
ALS_CONTR = 0x80, // ALS operation mode control and SW reset
PS_CONTR = 0x81, // PS operation mode control
PS_LED = 0x82, // PS LED pulse frequency control
PS_N_PULSES = 0x83, // PS number of pulses control
PS_MEAS_RATE = 0x84, // PS measurement rate in active mode
MEAS_RATE = 0x85, // ALS measurement rate in active mode
PART_ID = 0x86, // Part Number ID and Revision ID
MANUFAC_ID = 0x87, // Manufacturer ID
ALS_DATA_CH1_0 = 0x88, // ALS measurement CH1 data, lower byte - infrared only
ALS_DATA_CH1_1 = 0x89, // ALS measurement CH1 data, upper byte - infrared only
ALS_DATA_CH0_0 = 0x8A, // ALS measurement CH0 data, lower byte - visible + infrared
ALS_DATA_CH0_1 = 0x8B, // ALS measurement CH0 data, upper byte - visible + infrared
ALS_PS_STATUS = 0x8C, // ALS PS new data status
PS_DATA_0 = 0x8D, // PS measurement data, lower byte
PS_DATA_1 = 0x8E, // PS measurement data, upper byte
ALS_PS_INTERRUPT = 0x8F, // Interrupt status
PS_THRES_UP_0 = 0x90, // PS interrupt upper threshold, lower byte
PS_THRES_UP_1 = 0x91, // PS interrupt upper threshold, upper byte
PS_THRES_LOW_0 = 0x92, // PS interrupt lower threshold, lower byte
PS_THRES_LOW_1 = 0x93, // PS interrupt lower threshold, upper byte
PS_OFFSET_1 = 0x94, // PS offset, upper byte
PS_OFFSET_0 = 0x95, // PS offset, lower byte
// 0x96 - reserved
ALS_THRES_UP_0 = 0x97, // ALS interrupt upper threshold, lower byte
ALS_THRES_UP_1 = 0x98, // ALS interrupt upper threshold, upper byte
ALS_THRES_LOW_0 = 0x99, // ALS interrupt lower threshold, lower byte
ALS_THRES_LOW_1 = 0x9A, // ALS interrupt lower threshold, upper byte
// 0x9B - reserved
// 0x9C - reserved
// 0x9D - reserved
INTERRUPT_PERSIST = 0x9E // Interrupt persistence filter
};
// ALS Sensor gain levels
enum AlsGain : uint8_t {
GAIN_1 = 0, // default
GAIN_2 = 1,
GAIN_4 = 2,
GAIN_8 = 3,
GAIN_48 = 6,
GAIN_96 = 7,
};
static const uint8_t GAINS_COUNT = 6;
// ALS Sensor integration times
enum IntegrationTime : uint8_t {
INTEGRATION_TIME_100MS = 0, // default
INTEGRATION_TIME_50MS = 1,
INTEGRATION_TIME_200MS = 2,
INTEGRATION_TIME_400MS = 3,
INTEGRATION_TIME_150MS = 4,
INTEGRATION_TIME_250MS = 5,
INTEGRATION_TIME_300MS = 6,
INTEGRATION_TIME_350MS = 7
};
static const uint8_t TIMES_COUNT = 8;
// ALS Sensor measurement repeat rate
enum MeasurementRepeatRate {
REPEAT_RATE_50MS = 0,
REPEAT_RATE_100MS = 1,
REPEAT_RATE_200MS = 2,
REPEAT_RATE_500MS = 3, // default
REPEAT_RATE_1000MS = 4,
REPEAT_RATE_2000MS = 5
};
// PS Sensor gain levels
enum PsGain : uint8_t {
PS_GAIN_16 = 0, // default
PS_GAIN_32 = 2,
PS_GAIN_64 = 3,
};
// PS Mode
enum PsMode : uint8_t {
PS_MODE_STANDBY_00 = 0, // default
PS_MODE_STANDBY_01 = 1,
PS_MODE_ACTIVE_10 = 2,
PS_MODE_ACTIVE_11 = 3,
};
// LED Pulse Modulation Frequency
enum PsLedFreq : uint8_t {
PS_LED_FREQ_30KHZ = 0,
PS_LED_FREQ_40KHZ = 1,
PS_LED_FREQ_50KHZ = 2,
PS_LED_FREQ_60KHZ = 3, // default
PS_LED_FREQ_70KHZ = 4,
PS_LED_FREQ_80KHZ = 5,
PS_LED_FREQ_90KHZ = 6,
PS_LED_FREQ_100KHZ = 7,
};
// LED current duty
enum PsLedDuty : uint8_t {
PS_LED_DUTY_25 = 0,
PS_LED_DUTY_50 = 1,
PS_LED_DUTY_75 = 2,
PS_LED_DUTY_100 = 3, // default
};
// LED pulsed current level
enum PsLedCurrent : uint8_t {
PS_LED_CURRENT_5MA = 0,
PS_LED_CURRENT_10MA = 1,
PS_LED_CURRENT_20MA = 2,
PS_LED_CURRENT_50MA = 3,
PS_LED_CURRENT_100MA = 4, // default
PS_LED_CURRENT_100MA1 = 5,
PS_LED_CURRENT_100MA2 = 6,
PS_LED_CURRENT_100MA3 = 7,
};
// PS measurement rate
enum PsMeasurementRate : uint8_t {
PS_MEAS_RATE_50MS = 0,
PS_MEAS_RATE_70MS = 1,
PS_MEAS_RATE_100MS = 2,
PS_MEAS_RATE_200MS = 3,
PS_MEAS_RATE_500MS = 4, // default
PS_MEAS_RATE_1000MS = 5,
PS_MEAS_RATE_2000MS = 6,
PS_MEAS_RATE_2000MS1 = 7,
PS_MEAS_RATE_10MS = 8,
};
//
// ALS_CONTR Register (0x80)
//
union AlsControlRegister {
uint8_t raw;
struct {
bool active_mode : 1;
bool sw_reset : 1;
AlsGain gain : 3;
uint8_t reserved : 3;
} __attribute__((packed));
};
//
// PS_CONTR Register (0x81)
//
union PsControlRegister {
uint8_t raw;
struct {
bool ps_mode_xxx : 1;
bool ps_mode_active : 1;
PsGain ps_gain : 2; // only LTR-659/558
bool reserved_4 : 1;
bool ps_saturation_indicator_enable : 1;
bool reserved_6 : 1;
bool reserved_7 : 1;
} __attribute__((packed));
};
//
// PS_LED Register (0x82)
//
union PsLedRegister {
uint8_t raw;
struct {
PsLedCurrent ps_led_current : 3;
PsLedDuty ps_led_duty : 2;
PsLedFreq ps_led_freq : 3;
} __attribute__((packed));
};
//
// PS_N_PULSES Register (0x83)
//
union PsNPulsesRegister {
uint8_t raw;
struct {
uint8_t number_of_pulses : 4;
uint8_t reserved : 4;
} __attribute__((packed));
};
//
// PS_MEAS_RATE Register (0x84)
//
union PsMeasurementRateRegister {
uint8_t raw;
struct {
PsMeasurementRate ps_measurement_rate : 4;
uint8_t reserved : 4;
} __attribute__((packed));
};
//
// ALS_MEAS_RATE Register (0x85)
//
union MeasurementRateRegister {
uint8_t raw;
struct {
MeasurementRepeatRate measurement_repeat_rate : 3;
IntegrationTime integration_time : 3;
bool reserved_6 : 1;
bool reserved_7 : 1;
} __attribute__((packed));
};
//
// PART_ID Register (0x86) (Read Only)
//
union PartIdRegister {
uint8_t raw;
struct {
uint8_t part_number_id : 4;
uint8_t revision_id : 4;
} __attribute__((packed));
};
//
// ALS_PS_STATUS Register (0x8C) (Read Only)
//
union AlsPsStatusRegister {
uint8_t raw;
struct {
bool ps_new_data : 1; // 0 - old data, 1 - new data
bool ps_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active
bool als_new_data : 1; // 0 - old data, 1 - new data
bool als_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active
AlsGain gain : 3; // current ALS gain
bool data_invalid : 1;
} __attribute__((packed));
};
//
// PS_DATA_1 Register (0x8E) (Read Only)
//
union PsData1Register {
uint8_t raw;
struct {
uint8_t ps_data_high : 3;
uint8_t reserved : 4;
bool ps_saturation_flag : 1;
} __attribute__((packed));
};
//
// INTERRUPT Register (0x8F) (Read Only)
//
union InterruptRegister {
uint8_t raw;
struct {
bool ps_interrupt : 1;
bool als_interrupt : 1;
bool interrupt_polarity : 1; // 0 - active low (default), 1 - active high
uint8_t reserved : 5;
} __attribute__((packed));
};
//
// INTERRUPT_PERSIST Register (0x9E)
//
union InterruptPersistRegister {
uint8_t raw;
struct {
uint8_t als_persist : 4; // 0 - every ALS cycle, 1 - every 2 ALS cycles, ... 15 - every 16 ALS cycles
uint8_t ps_persist : 4; // 0 - every PS cycle, 1 - every 2 PS cycles, ... 15 - every 16 PS cycles
} __attribute__((packed));
};
} // namespace ltr_als_ps
} // namespace esphome

View file

@ -0,0 +1,271 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ACTUAL_GAIN,
CONF_AMBIENT_LIGHT,
CONF_AUTO_MODE,
CONF_GAIN,
CONF_GLASS_ATTENUATION_FACTOR,
CONF_ID,
CONF_INTEGRATION_TIME,
CONF_NAME,
CONF_REPEAT,
CONF_TRIGGER_ID,
CONF_TYPE,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_ILLUMINANCE,
ICON_BRIGHTNESS_5,
ICON_BRIGHTNESS_6,
ICON_TIMER,
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
UNIT_MILLISECOND,
)
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["i2c"]
CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time"
CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts"
CONF_INFRARED_COUNTS = "infrared_counts"
CONF_ON_PS_HIGH_THRESHOLD = "on_ps_high_threshold"
CONF_ON_PS_LOW_THRESHOLD = "on_ps_low_threshold"
CONF_PS_COOLDOWN = "ps_cooldown"
CONF_PS_COUNTS = "ps_counts"
CONF_PS_GAIN = "ps_gain"
CONF_PS_HIGH_THRESHOLD = "ps_high_threshold"
CONF_PS_LOW_THRESHOLD = "ps_low_threshold"
ICON_BRIGHTNESS_7 = "mdi:brightness-7"
ICON_GAIN = "mdi:multiplication"
ICON_PROXIMITY = "mdi:hand-wave-outline"
UNIT_COUNTS = "#"
ltr_als_ps_ns = cg.esphome_ns.namespace("ltr_als_ps")
LTRAlsPsComponent = ltr_als_ps_ns.class_(
"LTRAlsPsComponent", cg.PollingComponent, i2c.I2CDevice
)
LtrType = ltr_als_ps_ns.enum("LtrType")
LTR_TYPES = {
"ALS": LtrType.LTR_TYPE_ALS_ONLY,
"PS": LtrType.LTR_TYPE_PS_ONLY,
"ALS_PS": LtrType.LTR_TYPE_ALS_AND_PS,
}
AlsGain = ltr_als_ps_ns.enum("AlsGain")
ALS_GAINS = {
"1X": AlsGain.GAIN_1,
"2X": AlsGain.GAIN_2,
"4X": AlsGain.GAIN_4,
"8X": AlsGain.GAIN_8,
"48X": AlsGain.GAIN_48,
"96X": AlsGain.GAIN_96,
}
IntegrationTime = ltr_als_ps_ns.enum("IntegrationTime")
INTEGRATION_TIMES = {
50: IntegrationTime.INTEGRATION_TIME_50MS,
100: IntegrationTime.INTEGRATION_TIME_100MS,
150: IntegrationTime.INTEGRATION_TIME_150MS,
200: IntegrationTime.INTEGRATION_TIME_200MS,
250: IntegrationTime.INTEGRATION_TIME_250MS,
300: IntegrationTime.INTEGRATION_TIME_300MS,
350: IntegrationTime.INTEGRATION_TIME_350MS,
400: IntegrationTime.INTEGRATION_TIME_400MS,
}
MeasurementRepeatRate = ltr_als_ps_ns.enum("MeasurementRepeatRate")
MEASUREMENT_REPEAT_RATES = {
50: MeasurementRepeatRate.REPEAT_RATE_50MS,
100: MeasurementRepeatRate.REPEAT_RATE_100MS,
200: MeasurementRepeatRate.REPEAT_RATE_200MS,
500: MeasurementRepeatRate.REPEAT_RATE_500MS,
1000: MeasurementRepeatRate.REPEAT_RATE_1000MS,
2000: MeasurementRepeatRate.REPEAT_RATE_2000MS,
}
PsGain = ltr_als_ps_ns.enum("PsGain")
PS_GAINS = {
"16X": PsGain.PS_GAIN_16,
"32X": PsGain.PS_GAIN_32,
"64X": PsGain.PS_GAIN_64,
}
LTRPsHighTrigger = ltr_als_ps_ns.class_(
"LTRPsHighTrigger", automation.Trigger.template()
)
LTRPsLowTrigger = ltr_als_ps_ns.class_("LTRPsLowTrigger", automation.Trigger.template())
def validate_integration_time(value):
value = cv.positive_time_period_milliseconds(value).total_milliseconds
return cv.enum(INTEGRATION_TIMES, int=True)(value)
def validate_repeat_rate(value):
value = cv.positive_time_period_milliseconds(value).total_milliseconds
return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value)
def validate_time_and_repeat_rate(config):
integraton_time = config[CONF_INTEGRATION_TIME]
repeat_rate = config[CONF_REPEAT]
if integraton_time > repeat_rate:
raise cv.Invalid(
f"Measurement repeat rate ({repeat_rate}ms) shall be greater or equal to integration time ({integraton_time}ms)"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LTRAlsPsComponent),
cv.Optional(CONF_TYPE, default="ALS_PS"): cv.enum(LTR_TYPES, upper=True),
cv.Optional(CONF_AUTO_MODE, default=True): cv.boolean,
cv.Optional(CONF_GAIN, default="1X"): cv.enum(ALS_GAINS, upper=True),
cv.Optional(
CONF_INTEGRATION_TIME, default="100ms"
): validate_integration_time,
cv.Optional(CONF_REPEAT, default="500ms"): validate_repeat_rate,
cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range(
min=1.0
),
cv.Optional(
CONF_PS_COOLDOWN, default="5s"
): cv.positive_time_period_seconds,
cv.Optional(CONF_PS_GAIN, default="16X"): cv.enum(PS_GAINS, upper=True),
cv.Optional(CONF_PS_HIGH_THRESHOLD, default=65535): cv.int_range(
min=0, max=65535
),
cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range(
min=0, max=65535
),
cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger),
}
),
cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger),
}
),
cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_LUX,
icon=ICON_BRIGHTNESS_6,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_INFRARED_COUNTS): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_COUNTS,
icon=ICON_BRIGHTNESS_5,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_FULL_SPECTRUM_COUNTS): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_COUNTS,
icon=ICON_BRIGHTNESS_7,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_PS_COUNTS): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_COUNTS,
icon=ICON_PROXIMITY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_ACTUAL_GAIN): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_GAIN,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_ACTUAL_INTEGRATION_TIME): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_MILLISECOND,
icon=ICON_TIMER,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x29)),
validate_time_and_repeat_rate,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if als_config := config.get(CONF_AMBIENT_LIGHT):
sens = await sensor.new_sensor(als_config)
cg.add(var.set_ambient_light_sensor(sens))
if infrared_cnt_config := config.get(CONF_INFRARED_COUNTS):
sens = await sensor.new_sensor(infrared_cnt_config)
cg.add(var.set_infrared_counts_sensor(sens))
if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS):
sens = await sensor.new_sensor(full_spect_cnt_config)
cg.add(var.set_full_spectrum_counts_sensor(sens))
if act_gain_config := config.get(CONF_ACTUAL_GAIN):
sens = await sensor.new_sensor(act_gain_config)
cg.add(var.set_actual_gain_sensor(sens))
if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME):
sens = await sensor.new_sensor(act_itime_config)
cg.add(var.set_actual_integration_time_sensor(sens))
if prox_cnt_config := config.get(CONF_PS_COUNTS):
sens = await sensor.new_sensor(prox_cnt_config)
cg.add(var.set_proximity_counts_sensor(sens))
for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []):
trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], prox_high_tr)
for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []):
trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], prox_low_tr)
cg.add(var.set_ltr_type(config[CONF_TYPE]))
cg.add(var.set_als_auto_mode(config[CONF_AUTO_MODE]))
cg.add(var.set_als_gain(config[CONF_GAIN]))
cg.add(var.set_als_integration_time(config[CONF_INTEGRATION_TIME]))
cg.add(var.set_als_meas_repeat_rate(config[CONF_REPEAT]))
cg.add(var.set_als_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR]))
cg.add(var.set_ps_cooldown_time_s(config[CONF_PS_COOLDOWN]))
cg.add(var.set_ps_gain(config[CONF_PS_GAIN]))
cg.add(var.set_ps_high_threshold(config[CONF_PS_HIGH_THRESHOLD]))
cg.add(var.set_ps_low_threshold(config[CONF_PS_LOW_THRESHOLD]))

View file

@ -329,11 +329,14 @@ async def to_code(config):
file: Path = base_dir / h.hexdigest()[:8] / model_config[CONF_FILE] file: Path = base_dir / h.hexdigest()[:8] / model_config[CONF_FILE]
elif model_config[CONF_TYPE] == TYPE_LOCAL: elif model_config[CONF_TYPE] == TYPE_LOCAL:
file = model_config[CONF_PATH] file = Path(model_config[CONF_PATH])
elif model_config[CONF_TYPE] == TYPE_HTTP: elif model_config[CONF_TYPE] == TYPE_HTTP:
file = _compute_local_file_path(model_config) / "manifest.json" file = _compute_local_file_path(model_config) / "manifest.json"
else:
raise ValueError("Unsupported config type: {model_config[CONF_TYPE]}")
manifest, data = _load_model_data(file) manifest, data = _load_model_data(file)
rhs = [HexInt(x) for x in data] rhs = [HexInt(x) for x in data]

View file

@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv 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.components import web_server
from esphome.const import ( from esphome.const import (
CONF_ABOVE, CONF_ABOVE,
CONF_BELOW, CONF_BELOW,
@ -18,6 +19,7 @@ from esphome.const import (
CONF_VALUE, CONF_VALUE,
CONF_OPERATION, CONF_OPERATION,
CONF_CYCLE, CONF_CYCLE,
CONF_WEB_SERVER_ID,
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
@ -167,7 +169,10 @@ NUMBER_OPERATION_OPTIONS = {
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
validate_unit_of_measurement = cv.string_strict validate_unit_of_measurement = cv.string_strict
NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( NUMBER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent),
cv.Optional(CONF_ON_VALUE): automation.validate_automation( cv.Optional(CONF_ON_VALUE): automation.validate_automation(
@ -187,6 +192,7 @@ NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e
cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True), cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
} }
)
) )
_UNDEF = object() _UNDEF = object()
@ -248,6 +254,10 @@ async def setup_number_core_(
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_number( async def register_number(
var, config, *, min_value: float, max_value: float, step: float var, config, *, min_value: float, max_value: float, step: float

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
CONF_ICON, CONF_ICON,
@ -10,6 +10,7 @@ from esphome.const import (
CONF_OPTION, CONF_OPTION,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_CYCLE, CONF_CYCLE,
CONF_MODE, CONF_MODE,
CONF_OPERATION, CONF_OPERATION,
@ -47,7 +48,10 @@ SELECT_OPERATION_OPTIONS = {
} }
SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( SELECT_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent),
cv.GenerateID(): cv.declare_id(Select), cv.GenerateID(): cv.declare_id(Select),
@ -57,6 +61,7 @@ SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e
} }
), ),
} }
)
) )
_UNDEF = object() _UNDEF = object()
@ -99,6 +104,10 @@ async def setup_select_core_(var, config, *, options: list[str]):
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_select(var, config, *, options: list[str]): async def register_select(var, config, *, options: list[str]):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -3,7 +3,7 @@ import math
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ABOVE, CONF_ABOVE,
@ -31,6 +31,7 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_WINDOW_SIZE, CONF_WINDOW_SIZE,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
CONF_VALUE, CONF_VALUE,
CONF_MIN_VALUE, CONF_MIN_VALUE,
@ -252,7 +253,10 @@ validate_accuracy_decimals = cv.int_
validate_icon = cv.icon validate_icon = cv.icon
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent),
cv.GenerateID(): cv.declare_id(Sensor), cv.GenerateID(): cv.declare_id(Sensor),
@ -277,7 +281,9 @@ SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
), ),
cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorRawStateTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
SensorRawStateTrigger
),
} }
), ),
cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation(
@ -289,6 +295,7 @@ SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
), ),
} }
)
) )
_UNDEF = object() _UNDEF = object()
@ -772,6 +779,10 @@ async def setup_sensor_core_(var, config):
else: else:
cg.add(mqtt_.set_expire_after(expire_after)) cg.add(mqtt_.set_expire_after(expire_after))
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_sensor(var, config): async def register_sensor(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import Condition, maybe_simple_id from esphome.automation import Condition, maybe_simple_id
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@ -10,6 +10,7 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_INVERTED, CONF_INVERTED,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_ON_TURN_OFF, CONF_ON_TURN_OFF,
CONF_ON_TURN_ON, CONF_ON_TURN_ON,
CONF_RESTORE_MODE, CONF_RESTORE_MODE,
@ -64,7 +65,10 @@ SwitchTurnOffTrigger = switch_ns.class_(
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True) validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True)
_SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( _SWITCH_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent),
cv.Optional(CONF_INVERTED): cv.boolean, cv.Optional(CONF_INVERTED): cv.boolean,
@ -80,6 +84,7 @@ _SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
), ),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
} }
)
) )
_UNDEF = object() _UNDEF = object()
@ -151,6 +156,10 @@ async def setup_switch_core_(var, config):
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class)) cg.add(var.set_device_class(device_class))

View file

@ -2,13 +2,14 @@ from typing import Optional
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_MODE, CONF_MODE,
CONF_ON_VALUE, CONF_ON_VALUE,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_VALUE, CONF_VALUE,
) )
@ -38,7 +39,10 @@ TEXT_MODES = {
"PASSWORD": TextMode.TEXT_MODE_PASSWORD, # to be implemented for keys, passwords, etc. "PASSWORD": TextMode.TEXT_MODE_PASSWORD, # to be implemented for keys, passwords, etc.
} }
TEXT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( TEXT_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextComponent),
cv.GenerateID(): cv.declare_id(Text), cv.GenerateID(): cv.declare_id(Text),
@ -49,6 +53,7 @@ TEXT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
), ),
cv.Required(CONF_MODE): cv.enum(TEXT_MODES, upper=True), cv.Required(CONF_MODE): cv.enum(TEXT_MODES, upper=True),
} }
)
) )
@ -77,6 +82,10 @@ async def setup_text_core_(
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_text( async def register_text(
var, var,

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@ -12,6 +12,7 @@ from esphome.const import (
CONF_ON_RAW_VALUE, CONF_ON_RAW_VALUE,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_WEB_SERVER_ID,
CONF_STATE, CONF_STATE,
CONF_FROM, CONF_FROM,
CONF_TO, CONF_TO,
@ -124,7 +125,10 @@ async def map_filter_to_code(config, filter_id):
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") 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(web_server.WEBSERVER_SORTING_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),
@ -132,7 +136,9 @@ TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).exte
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(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TextSensorStateTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
TextSensorStateTrigger
),
} }
), ),
cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation(
@ -143,6 +149,7 @@ TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).exte
} }
), ),
} }
)
) )
_UNDEF = object() _UNDEF = object()
@ -205,6 +212,10 @@ async def setup_text_sensor_core_(var, config):
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_text_sensor(var, config): async def register_text_sensor(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id, Condition from esphome.automation import maybe_simple_id, Condition
from esphome.components import mqtt from esphome.components import mqtt, web_server
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ID, CONF_ID,
@ -14,6 +14,7 @@ from esphome.const import (
CONF_STATE, CONF_STATE,
CONF_STOP, CONF_STOP,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_WEB_SERVER_ID,
DEVICE_CLASS_EMPTY, DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GAS, DEVICE_CLASS_GAS,
DEVICE_CLASS_WATER, DEVICE_CLASS_WATER,
@ -70,7 +71,10 @@ ValveClosedTrigger = valve_ns.class_(
CONF_ON_CLOSED = "on_closed" CONF_ON_CLOSED = "on_closed"
VALVE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( VALVE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Valve), cv.GenerateID(): cv.declare_id(Valve),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTValveComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTValveComponent),
@ -92,6 +96,7 @@ VALVE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).ex
} }
), ),
} }
)
) )
@ -119,6 +124,10 @@ async def setup_valve_core_(var, config):
mqtt_.set_custom_position_command_topic(position_command_topic_config) mqtt_.set_custom_position_command_topic(position_command_topic_config)
) )
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config)
async def register_valve(var, config): async def register_valve(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):

View file

@ -3,6 +3,7 @@ import esphome.config_validation as cv
from esphome.components import i2c, sensor from esphome.components import i2c, sensor
from esphome.const import ( from esphome.const import (
CONF_ACTUAL_GAIN, CONF_ACTUAL_GAIN,
CONF_AMBIENT_LIGHT,
CONF_AUTO_MODE, CONF_AUTO_MODE,
CONF_FULL_SPECTRUM, CONF_FULL_SPECTRUM,
CONF_GAIN, CONF_GAIN,
@ -11,13 +12,13 @@ from esphome.const import (
CONF_INFRARED, CONF_INFRARED,
CONF_INTEGRATION_TIME, CONF_INTEGRATION_TIME,
CONF_NAME, CONF_NAME,
UNIT_LUX, DEVICE_CLASS_ILLUMINANCE,
UNIT_MILLISECOND,
ICON_BRIGHTNESS_5, ICON_BRIGHTNESS_5,
ICON_BRIGHTNESS_6, ICON_BRIGHTNESS_6,
ICON_TIMER, ICON_TIMER,
DEVICE_CLASS_ILLUMINANCE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_LUX,
UNIT_MILLISECOND,
) )
CODEOWNERS = ["@latonita"] CODEOWNERS = ["@latonita"]
@ -28,7 +29,6 @@ ICON_MULTIPLICATION = "mdi:multiplication"
ICON_BRIGHTNESS_7 = "mdi:brightness-7" ICON_BRIGHTNESS_7 = "mdi:brightness-7"
CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time" CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time"
CONF_AMBIENT_LIGHT = "ambient_light"
CONF_AMBIENT_LIGHT_COUNTS = "ambient_light_counts" CONF_AMBIENT_LIGHT_COUNTS = "ambient_light_counts"
CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts" CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts"
CONF_LUX_COMPENSATION = "lux_compensation" CONF_LUX_COMPENSATION = "lux_compensation"

View file

@ -44,6 +44,12 @@ CONF_VOLUME_MULTIPLIER = "volume_multiplier"
CONF_WAKE_WORD = "wake_word" CONF_WAKE_WORD = "wake_word"
CONF_ON_TIMER_STARTED = "on_timer_started"
CONF_ON_TIMER_UPDATED = "on_timer_updated"
CONF_ON_TIMER_CANCELLED = "on_timer_cancelled"
CONF_ON_TIMER_FINISHED = "on_timer_finished"
CONF_ON_TIMER_TICK = "on_timer_tick"
voice_assistant_ns = cg.esphome_ns.namespace("voice_assistant") voice_assistant_ns = cg.esphome_ns.namespace("voice_assistant")
VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component) VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component)
@ -64,6 +70,8 @@ ConnectedCondition = voice_assistant_ns.class_(
"ConnectedCondition", automation.Condition, cg.Parented.template(VoiceAssistant) "ConnectedCondition", automation.Condition, cg.Parented.template(VoiceAssistant)
) )
Timer = voice_assistant_ns.struct("Timer")
def tts_stream_validate(config): def tts_stream_validate(config):
if CONF_SPEAKER not in config and ( if CONF_SPEAKER not in config and (
@ -131,6 +139,21 @@ CONFIG_SCHEMA = cv.All(
single=True single=True
), ),
cv.Optional(CONF_ON_IDLE): automation.validate_automation(single=True), cv.Optional(CONF_ON_IDLE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_TIMER_STARTED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_TIMER_UPDATED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_TIMER_CANCELLED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_TIMER_FINISHED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_TIMER_TICK): automation.validate_automation(
single=True
),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
tts_stream_validate, tts_stream_validate,
@ -270,6 +293,49 @@ async def to_code(config):
config[CONF_ON_IDLE], config[CONF_ON_IDLE],
) )
has_timers = False
if on_timer_started := config.get(CONF_ON_TIMER_STARTED):
await automation.build_automation(
var.get_timer_started_trigger(),
[(Timer, "timer")],
on_timer_started,
)
has_timers = True
if on_timer_updated := config.get(CONF_ON_TIMER_UPDATED):
await automation.build_automation(
var.get_timer_updated_trigger(),
[(Timer, "timer")],
on_timer_updated,
)
has_timers = True
if on_timer_cancelled := config.get(CONF_ON_TIMER_CANCELLED):
await automation.build_automation(
var.get_timer_cancelled_trigger(),
[(Timer, "timer")],
on_timer_cancelled,
)
has_timers = True
if on_timer_finished := config.get(CONF_ON_TIMER_FINISHED):
await automation.build_automation(
var.get_timer_finished_trigger(),
[(Timer, "timer")],
on_timer_finished,
)
has_timers = True
if on_timer_tick := config.get(CONF_ON_TIMER_TICK):
await automation.build_automation(
var.get_timer_tick_trigger(),
[(cg.std_vector.template(Timer), "timers")],
on_timer_tick,
)
has_timers = True
cg.add(var.set_has_timers(has_timers))
cg.add_define("USE_VOICE_ASSISTANT") cg.add_define("USE_VOICE_ASSISTANT")

View file

@ -18,7 +18,7 @@ static const char *const TAG = "voice_assistant";
static const size_t SAMPLE_RATE_HZ = 16000; static const size_t SAMPLE_RATE_HZ = 16000;
static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms
static const size_t BUFFER_SIZE = 1024 * SAMPLE_RATE_HZ / 1000; static const size_t BUFFER_SIZE = 512 * SAMPLE_RATE_HZ / 1000;
static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t); static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t);
static const size_t RECEIVE_SIZE = 1024; static const size_t RECEIVE_SIZE = 1024;
static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE; static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE;
@ -798,12 +798,65 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
this->speaker_buffer_index_ += msg.data.length(); this->speaker_buffer_index_ += msg.data.length();
this->speaker_buffer_size_ += msg.data.length(); this->speaker_buffer_size_ += msg.data.length();
this->speaker_bytes_received_ += msg.data.length(); this->speaker_bytes_received_ += msg.data.length();
ESP_LOGD(TAG, "Received audio: %d bytes from API", msg.data.length());
} else { } else {
ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
} }
#endif #endif
} }
void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) {
Timer timer = {
.id = msg.timer_id,
.name = msg.name,
.total_seconds = msg.total_seconds,
.seconds_left = msg.seconds_left,
.is_active = msg.is_active,
};
this->timers_[timer.id] = timer;
ESP_LOGD(TAG, "Timer Event");
ESP_LOGD(TAG, " Type: %d", msg.event_type);
ESP_LOGD(TAG, " %s", timer.to_string().c_str());
switch (msg.event_type) {
case api::enums::VOICE_ASSISTANT_TIMER_STARTED:
this->timer_started_trigger_->trigger(timer);
break;
case api::enums::VOICE_ASSISTANT_TIMER_UPDATED:
this->timer_updated_trigger_->trigger(timer);
break;
case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED:
this->timer_cancelled_trigger_->trigger(timer);
this->timers_.erase(timer.id);
break;
case api::enums::VOICE_ASSISTANT_TIMER_FINISHED:
this->timer_finished_trigger_->trigger(timer);
this->timers_.erase(timer.id);
break;
}
if (this->timers_.empty()) {
this->cancel_interval("timer-event");
this->timer_tick_running_ = false;
} else if (!this->timer_tick_running_) {
this->set_interval("timer-event", 1000, [this]() { this->timer_tick_(); });
this->timer_tick_running_ = true;
}
}
void VoiceAssistant::timer_tick_() {
std::vector<Timer> res;
res.reserve(this->timers_.size());
for (auto &pair : this->timers_) {
auto &timer = pair.second;
if (timer.is_active && timer.seconds_left > 0) {
timer.seconds_left--;
}
res.push_back(timer);
}
this->timer_tick_trigger_->trigger(res);
}
VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace voice_assistant } // namespace voice_assistant

View file

@ -24,6 +24,9 @@
#include <esp_vad.h> #include <esp_vad.h>
#endif #endif
#include <unordered_map>
#include <vector>
namespace esphome { namespace esphome {
namespace voice_assistant { namespace voice_assistant {
@ -36,6 +39,7 @@ enum VoiceAssistantFeature : uint32_t {
FEATURE_VOICE_ASSISTANT = 1 << 0, FEATURE_VOICE_ASSISTANT = 1 << 0,
FEATURE_SPEAKER = 1 << 1, FEATURE_SPEAKER = 1 << 1,
FEATURE_API_AUDIO = 1 << 2, FEATURE_API_AUDIO = 1 << 2,
FEATURE_TIMERS = 1 << 3,
}; };
enum class State { enum class State {
@ -59,6 +63,20 @@ enum AudioMode : uint8_t {
AUDIO_MODE_API, AUDIO_MODE_API,
}; };
struct Timer {
std::string id;
std::string name;
uint32_t total_seconds;
uint32_t seconds_left;
bool is_active;
std::string to_string() const {
return str_sprintf("Timer(id=%s, name=%s, total_seconds=%" PRIu32 ", seconds_left=%" PRIu32 ", is_active=%s)",
this->id.c_str(), this->name.c_str(), this->total_seconds, this->seconds_left,
YESNO(this->is_active));
}
};
class VoiceAssistant : public Component { class VoiceAssistant : public Component {
public: public:
void setup() override; void setup() override;
@ -100,6 +118,11 @@ class VoiceAssistant : public Component {
flags |= VoiceAssistantFeature::FEATURE_SPEAKER; flags |= VoiceAssistantFeature::FEATURE_SPEAKER;
} }
#endif #endif
if (this->has_timers_) {
flags |= VoiceAssistantFeature::FEATURE_TIMERS;
}
return flags; return flags;
} }
@ -108,6 +131,7 @@ class VoiceAssistant : public Component {
void on_event(const api::VoiceAssistantEventResponse &msg); void on_event(const api::VoiceAssistantEventResponse &msg);
void on_audio(const api::VoiceAssistantAudio &msg); void on_audio(const api::VoiceAssistantAudio &msg);
void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg);
bool is_running() const { return this->state_ != State::IDLE; } bool is_running() const { return this->state_ != State::IDLE; }
void set_continuous(bool continuous) { this->continuous_ = continuous; } void set_continuous(bool continuous) { this->continuous_ = continuous; }
@ -150,6 +174,14 @@ class VoiceAssistant : public Component {
void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; } void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; }
Trigger<Timer> *get_timer_started_trigger() const { return this->timer_started_trigger_; }
Trigger<Timer> *get_timer_updated_trigger() const { return this->timer_updated_trigger_; }
Trigger<Timer> *get_timer_cancelled_trigger() const { return this->timer_cancelled_trigger_; }
Trigger<Timer> *get_timer_finished_trigger() const { return this->timer_finished_trigger_; }
Trigger<std::vector<Timer>> *get_timer_tick_trigger() const { return this->timer_tick_trigger_; }
void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; }
const std::unordered_map<std::string, Timer> &get_timers() const { return this->timers_; }
protected: protected:
bool allocate_buffers_(); bool allocate_buffers_();
void clear_buffers_(); void clear_buffers_();
@ -186,6 +218,16 @@ class VoiceAssistant : public Component {
api::APIConnection *api_client_{nullptr}; api::APIConnection *api_client_{nullptr};
std::unordered_map<std::string, Timer> timers_;
void timer_tick_();
Trigger<Timer> *timer_started_trigger_ = new Trigger<Timer>();
Trigger<Timer> *timer_finished_trigger_ = new Trigger<Timer>();
Trigger<Timer> *timer_updated_trigger_ = new Trigger<Timer>();
Trigger<Timer> *timer_cancelled_trigger_ = new Trigger<Timer>();
Trigger<std::vector<Timer>> *timer_tick_trigger_ = new Trigger<std::vector<Timer>>();
bool has_timers_{false};
bool timer_tick_running_{false};
microphone::Microphone *mic_{nullptr}; microphone::Microphone *mic_{nullptr};
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
void write_speaker_(); void write_speaker_();

View file

@ -1,6 +1,9 @@
from __future__ import annotations
import gzip import gzip
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components import web_server_base from esphome.components import web_server_base
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
from esphome.const import ( from esphome.const import (
@ -19,6 +22,8 @@ from esphome.const import (
CONF_LOG, CONF_LOG,
CONF_VERSION, CONF_VERSION,
CONF_LOCAL, CONF_LOCAL,
CONF_WEB_SERVER_ID,
CONF_WEB_SERVER_SORTING_WEIGHT,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_BK72XX, PLATFORM_BK72XX,
@ -64,6 +69,46 @@ def validate_ota(config):
return config return config
def _validate_no_sorting_weight(
webserver_version: int, config: dict, path: list[str] | None = None
) -> None:
if path is None:
path = []
if CONF_WEB_SERVER_SORTING_WEIGHT in config:
raise cv.FinalExternalInvalid(
f"Sorting weight on entities is not supported in web_server version {webserver_version}",
path=path + [CONF_WEB_SERVER_SORTING_WEIGHT],
)
for p, value in config.items():
if isinstance(value, dict):
_validate_no_sorting_weight(webserver_version, value, path + [p])
elif isinstance(value, list):
for i, item in enumerate(value):
if isinstance(item, dict):
_validate_no_sorting_weight(webserver_version, item, path + [p, i])
def _final_validate_sorting_weight(config):
if (webserver_version := config.get(CONF_VERSION)) != 3:
_validate_no_sorting_weight(webserver_version, fv.full_config.get())
return config
FINAL_VALIDATE_SCHEMA = _final_validate_sorting_weight
WEBSERVER_SORTING_SCHEMA = cv.Schema(
{
cv.OnlyWith(CONF_WEB_SERVER_ID, "web_server"): cv.use_id(WebServer),
cv.Optional(CONF_WEB_SERVER_SORTING_WEIGHT): cv.All(
cv.requires_component("web_server"),
cv.float_,
),
}
)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@ -108,6 +153,19 @@ CONFIG_SCHEMA = cv.All(
) )
def add_entity_to_sorting_list(web_server, entity, config):
sorting_weight = 50
if CONF_WEB_SERVER_SORTING_WEIGHT in config:
sorting_weight = config[CONF_WEB_SERVER_SORTING_WEIGHT]
cg.add(
web_server.add_entity_to_sorting_list(
entity,
sorting_weight,
)
)
def build_index_html(config) -> str: def build_index_html(config) -> str:
html = "<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>" html = "<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>"
css_include = config.get(CONF_CSS_INCLUDE) css_include = config.get(CONF_CSS_INCLUDE)

View file

@ -435,7 +435,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
request->send(404); request->send(404);
} }
std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) { return json::build_json([this, obj, value, start_config](JsonObject root) {
std::string state; std::string state;
if (std::isnan(value)) { if (std::isnan(value)) {
state = "NA"; state = "NA";
@ -446,6 +446,9 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
} }
set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
if (!obj->get_unit_of_measurement().empty()) if (!obj->get_unit_of_measurement().empty())
root["uom"] = obj->get_unit_of_measurement(); root["uom"] = obj->get_unit_of_measurement();
} }
@ -471,8 +474,13 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
} }
std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config) { JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) { return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
}); });
} }
#endif #endif
@ -483,14 +491,6 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
return; return;
this->events_.send(this->switch_json(obj, state, DETAIL_STATE).c_str(), "state"); this->events_.send(this->switch_json(obj, state, DETAIL_STATE).c_str(), "state");
} }
std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config);
if (start_config == DETAIL_ALL) {
root["assumed_state"] = obj->assumed_state();
}
});
}
void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (switch_::Switch *obj : App.get_switches()) { for (switch_::Switch *obj : App.get_switches()) {
if (obj->get_object_id() != match.id) if (obj->get_object_id() != match.id)
@ -515,14 +515,20 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
} }
request->send(404); request->send(404);
} }
std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) {
return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config);
if (start_config == DETAIL_ALL) {
root["assumed_state"] = obj->assumed_state();
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
});
}
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) {
return json::build_json(
[obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); });
}
void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (button::Button *obj : App.get_buttons()) { for (button::Button *obj : App.get_buttons()) {
if (obj->get_object_id() != match.id) if (obj->get_object_id() != match.id)
@ -538,6 +544,16 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
} }
request->send(404); request->send(404);
} }
std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) {
return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "button-" + obj->get_object_id(), start_config);
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
});
}
#endif #endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@ -546,12 +562,6 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool s
return; return;
this->events_.send(this->binary_sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); this->events_.send(this->binary_sensor_json(obj, state, DETAIL_STATE).c_str(), "state");
} }
std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value,
start_config);
});
}
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
if (obj->get_object_id() != match.id) if (obj->get_object_id() != match.id)
@ -562,6 +572,17 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
} }
request->send(404); request->send(404);
} }
std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value,
start_config);
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
});
}
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
@ -570,19 +591,6 @@ void WebServer::on_fan_update(fan::Fan *obj) {
return; return;
this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state");
} }
std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state,
start_config);
const auto traits = obj->get_traits();
if (traits.supports_speed()) {
root["speed_level"] = obj->speed;
root["speed_count"] = traits.supported_speed_count();
}
if (obj->get_traits().supports_oscillation())
root["oscillation"] = obj->oscillating;
});
}
void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (fan::Fan *obj : App.get_fans()) { for (fan::Fan *obj : App.get_fans()) {
if (obj->get_object_id() != match.id) if (obj->get_object_id() != match.id)
@ -635,6 +643,24 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
} }
request->send(404); request->send(404);
} }
std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
return json::build_json([this, obj, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state,
start_config);
const auto traits = obj->get_traits();
if (traits.supports_speed()) {
root["speed_level"] = obj->speed;
root["speed_count"] = traits.supported_speed_count();
}
if (obj->get_traits().supports_oscillation())
root["oscillation"] = obj->oscillating;
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
});
}
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
@ -729,7 +755,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
request->send(404); request->send(404);
} }
std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); set_json_id(root, obj, "light-" + obj->get_object_id(), start_config);
root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; root["state"] = obj->remote_values.is_on() ? "ON" : "OFF";
@ -740,6 +766,9 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
for (auto const &option : obj->get_effects()) { for (auto const &option : obj->get_effects()) {
opt.add(option->get_name()); opt.add(option->get_name());
} }
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
} }
}); });
} }
@ -803,7 +832,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
request->send(404); request->send(404);
} }
std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
obj->position, start_config); obj->position, start_config);
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
@ -812,6 +841,11 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
root["position"] = obj->position; root["position"] = obj->position;
if (obj->get_traits().get_supports_tilt()) if (obj->get_traits().get_supports_tilt())
root["tilt"] = obj->tilt; root["tilt"] = obj->tilt;
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
}); });
} }
#endif #endif
@ -852,7 +886,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
} }
std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) { return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); set_json_id(root, obj, "number-" + obj->get_object_id(), start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
root["min_value"] = root["min_value"] =
@ -864,6 +898,9 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
root["mode"] = (int) obj->traits.get_mode(); root["mode"] = (int) obj->traits.get_mode();
if (!obj->traits.get_unit_of_measurement().empty()) if (!obj->traits.get_unit_of_measurement().empty())
root["uom"] = obj->traits.get_unit_of_measurement(); root["uom"] = obj->traits.get_unit_of_measurement();
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
} }
if (std::isnan(value)) { if (std::isnan(value)) {
root["value"] = "\"NaN\""; root["value"] = "\"NaN\"";
@ -919,11 +956,16 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
} }
std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); set_json_id(root, obj, "date-" + obj->get_object_id(), start_config);
std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day);
root["value"] = value; root["value"] = value;
root["state"] = value; root["state"] = value;
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
}); });
} }
#endif // USE_DATETIME_DATE #endif // USE_DATETIME_DATE
@ -967,11 +1009,16 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
request->send(404); request->send(404);
} }
std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) { std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); set_json_id(root, obj, "time-" + obj->get_object_id(), start_config);
std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
root["value"] = value; root["value"] = value;
root["state"] = value; root["state"] = value;
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
}); });
} }
#endif // USE_DATETIME_TIME #endif // USE_DATETIME_TIME
@ -1015,12 +1062,17 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
request->send(404); request->send(404);
} }
std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config);
std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour,
obj->minute, obj->second); obj->minute, obj->second);
root["value"] = value; root["value"] = value;
root["state"] = value; root["state"] = value;
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
}); });
} }
#endif // USE_DATETIME_DATETIME #endif // USE_DATETIME_DATETIME
@ -1060,11 +1112,8 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
} }
std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) { std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) { return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); set_json_id(root, obj, "text-" + obj->get_object_id(), start_config);
if (start_config == DETAIL_ALL) {
root["mode"] = (int) obj->traits.get_mode();
}
root["min_length"] = obj->traits.get_min_length(); root["min_length"] = obj->traits.get_min_length();
root["max_length"] = obj->traits.get_max_length(); root["max_length"] = obj->traits.get_max_length();
root["pattern"] = obj->traits.get_pattern(); root["pattern"] = obj->traits.get_pattern();
@ -1074,6 +1123,12 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
root["state"] = value; root["state"] = value;
} }
root["value"] = value; root["value"] = value;
if (start_config == DETAIL_ALL) {
root["mode"] = (int) obj->traits.get_mode();
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
}); });
} }
#endif #endif
@ -1119,13 +1174,16 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
request->send(404); request->send(404);
} }
std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) { return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("option"); JsonArray opt = root.createNestedArray("option");
for (auto &option : obj->traits.get_options()) { for (auto &option : obj->traits.get_options()) {
opt.add(option); opt.add(option);
} }
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
} }
}); });
} }
@ -1140,7 +1198,6 @@ void WebServer::on_climate_update(climate::Climate *obj) {
return; return;
this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state"); this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state");
} }
void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_climates()) { for (auto *obj : App.get_climates()) {
if (obj->get_object_id() != match.id) if (obj->get_object_id() != match.id)
@ -1188,9 +1245,8 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
} }
request->send(404); request->send(404);
} }
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
const auto traits = obj->get_traits(); const auto traits = obj->get_traits();
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
@ -1227,6 +1283,9 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
for (auto const &custom_preset : traits.get_supported_custom_presets()) for (auto const &custom_preset : traits.get_supported_custom_presets())
opt.add(custom_preset); opt.add(custom_preset);
} }
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
} }
bool has_state = false; bool has_state = false;
@ -1283,12 +1342,6 @@ void WebServer::on_lock_update(lock::Lock *obj) {
return; return;
this->events_.send(this->lock_json(obj, obj->state, DETAIL_STATE).c_str(), "state"); this->events_.send(this->lock_json(obj, obj->state, DETAIL_STATE).c_str(), "state");
} }
std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value,
start_config);
});
}
void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (lock::Lock *obj : App.get_locks()) { for (lock::Lock *obj : App.get_locks()) {
if (obj->get_object_id() != match.id) if (obj->get_object_id() != match.id)
@ -1313,6 +1366,17 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
} }
request->send(404); request->send(404);
} }
std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
return json::build_json([this, obj, value, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value,
start_config);
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
});
}
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
@ -1366,13 +1430,16 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
request->send(404); request->send(404);
} }
std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { return json::build_json([this, obj, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
obj->position, start_config); obj->position, start_config);
root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); root["current_operation"] = valve::valve_operation_to_str(obj->current_operation);
if (obj->get_traits().get_supports_position()) if (obj->get_traits().get_supports_position())
root["position"] = obj->position; root["position"] = obj->position;
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}); });
} }
#endif #endif
@ -1383,15 +1450,6 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP
return; return;
this->events_.send(this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE).c_str(), "state"); this->events_.send(this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE).c_str(), "state");
} }
std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) {
char buf[16];
set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(),
PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config);
});
}
void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) {
if (obj->get_object_id() != match.id) if (obj->get_object_id() != match.id)
@ -1405,6 +1463,20 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
} }
request->send(404); request->send(404);
} }
std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) {
return json::build_json([this, obj, value, start_config](JsonObject root) {
char buf[16];
set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(),
PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config);
if (start_config == DETAIL_ALL) {
if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) {
root["sorting_weight"] = this->sorting_entitys_[obj].weight;
}
}
});
}
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
@ -1709,6 +1781,10 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
bool WebServer::isRequestHandlerTrivial() { return false; } bool WebServer::isRequestHandlerTrivial() { return false; }
void WebServer::add_entity_to_sorting_list(EntityBase *entity, float weight) {
this->sorting_entitys_[entity] = SortingComponents{weight};
}
void WebServer::schedule_(std::function<void()> &&f) { void WebServer::schedule_(std::function<void()> &&f) {
#ifdef USE_ESP32 #ifdef USE_ESP32
xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY); xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY);

View file

@ -5,8 +5,10 @@
#include "esphome/components/web_server_base/web_server_base.h" #include "esphome/components/web_server_base/web_server_base.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/controller.h" #include "esphome/core/controller.h"
#include "esphome/core/entity_base.h"
#include <vector> #include <vector>
#include <map>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
@ -39,6 +41,10 @@ struct UrlMatch {
bool valid; ///< Whether this match is valid bool valid; ///< Whether this match is valid
}; };
struct SortingComponents {
float weight;
};
enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
/** This class allows users to create a web server with their ESP nodes. /** This class allows users to create a web server with their ESP nodes.
@ -320,12 +326,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
/// This web handle is not trivial. /// This web handle is not trivial.
bool isRequestHandlerTrivial() override; bool isRequestHandlerTrivial() override;
void add_entity_to_sorting_list(EntityBase *entity, float weight);
protected: protected:
void schedule_(std::function<void()> &&f); void schedule_(std::function<void()> &&f);
friend ListEntitiesIterator; friend ListEntitiesIterator;
web_server_base::WebServerBase *base_; web_server_base::WebServerBase *base_;
AsyncEventSource events_{"/events"}; AsyncEventSource events_{"/events"};
ListEntitiesIterator entities_iterator_; ListEntitiesIterator entities_iterator_;
std::map<EntityBase *, SortingComponents> sorting_entitys_;
#if USE_WEBSERVER_VERSION == 1 #if USE_WEBSERVER_VERSION == 1
const char *css_url_{nullptr}; const char *css_url_{nullptr};
const char *js_url_{nullptr}; const char *js_url_{nullptr};

View file

@ -49,6 +49,7 @@ CONF_AFTER = "after"
CONF_ALLOW_OTHER_USES = "allow_other_uses" CONF_ALLOW_OTHER_USES = "allow_other_uses"
CONF_ALPHA = "alpha" CONF_ALPHA = "alpha"
CONF_ALTITUDE = "altitude" CONF_ALTITUDE = "altitude"
CONF_AMBIENT_LIGHT = "ambient_light"
CONF_ANALOG = "analog" CONF_ANALOG = "analog"
CONF_AND = "and" CONF_AND = "and"
CONF_ANGLE = "angle" CONF_ANGLE = "angle"
@ -904,6 +905,8 @@ CONF_WARM_WHITE = "warm_white"
CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature"
CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold"
CONF_WEB_SERVER = "web_server" CONF_WEB_SERVER = "web_server"
CONF_WEB_SERVER_ID = "web_server_id"
CONF_WEB_SERVER_SORTING_WEIGHT = "web_server_sorting_weight"
CONF_WEIGHT = "weight" CONF_WEIGHT = "weight"
CONF_WHILE = "while" CONF_WHILE = "while"
CONF_WHITE = "white" CONF_WHITE = "white"

View file

@ -1,11 +1,11 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <cinttypes>
#include <utility>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <utility>
#include <cinttypes>
namespace esphome { namespace esphome {
@ -140,8 +140,8 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
float backoff_increase_factor) { // NOLINT float backoff_increase_factor) { // NOLINT
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
} }
bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
bool Component::is_ready() { bool Component::is_ready() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
} }

View file

@ -118,9 +118,9 @@ class Component {
*/ */
virtual void mark_failed(); virtual void mark_failed();
bool is_failed(); bool is_failed() const;
bool is_ready(); bool is_ready() const;
virtual bool can_proceed(); virtual bool can_proceed();

View file

@ -0,0 +1,11 @@
sensor:
- platform: ltr_als_ps
address: 0x23
i2c_id: i2c_als_ps
gain: 1x
integration_time: 100ms
ps_cooldown: 5 s
ambient_light: "Ambient light"
full_spectrum_counts: "Full spectrum counts"
infrared_counts: "Infrared counts"
actual_gain: "Actual gain"

View file

@ -0,0 +1,6 @@
i2c:
- id: i2c_als_ps
scl: 5
sda: 4
<<: !include common.yaml

View file

@ -0,0 +1,6 @@
i2c:
- id: i2c_als_ps
scl: 5
sda: 4
<<: !include common.yaml

View file

@ -0,0 +1,6 @@
i2c:
- id: i2c_als_ps
scl: 16
sda: 17
<<: !include common.yaml

View file

@ -0,0 +1,6 @@
i2c:
- id: i2c_als_ps
scl: 16
sda: 17
<<: !include common.yaml

View file

@ -0,0 +1,6 @@
i2c:
- id: i2c_als_ps
scl: 5
sda: 4
<<: !include common.yaml

View file

@ -0,0 +1,6 @@
i2c:
- id: i2c_als_ps
scl: 5
sda: 4
<<: !include common.yaml