[voice_assistant] Timers (#6821)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
Jesse Hills 2024-05-30 13:09:19 +12:00 committed by GitHub
parent 5ae32e81c3
commit 854d3f2e4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 321 additions and 0 deletions

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

@ -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

@ -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_();