iobroker.assistant 1.4.7 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/admin/custom/.vite/manifest.json +1 -1
- package/admin/custom/assets/{Components-BR57MZLJ.js → Components-srYDLjt6.js} +2 -2
- package/admin/custom/customComponents.js +1 -1
- package/admin/custom/index.html +1 -1
- package/admin/custom/mf-manifest.json +1 -1
- package/admin/custom/mf-stats.json +1 -1
- package/build/lib/piAgent.js +2 -0
- package/build/lib/piAgent.js.map +1 -1
- package/build/lib/timers.js +7 -0
- package/build/lib/timers.js.map +1 -1
- package/build/lib/voice/esphome.js +49 -25
- package/build/lib/voice/esphome.js.map +1 -1
- package/build/lib/voice/esphomeEntities.js +52 -0
- package/build/lib/voice/esphomeEntities.js.map +1 -1
- package/build/lib/voice/esphomeProto.js.map +1 -1
- package/build/main.js +97 -19
- package/build/main.js.map +1 -1
- package/io-package.json +27 -27
- package/package.json +1 -1
|
@@ -182,10 +182,12 @@ const CONFIRM_ENTITY = 'confirm_answer';
|
|
|
182
182
|
/** Select entity of a satellite with a screen: the language of the date and the hints it shows. */
|
|
183
183
|
const DISPLAY_LANGUAGE_ENTITY = 'display_language';
|
|
184
184
|
/**
|
|
185
|
-
* After a reply that asks something: how long, beyond the reply's own length, the
|
|
186
|
-
*
|
|
185
|
+
* After a reply that asks something: how long, beyond the reply's own length, the question stays open — the next
|
|
186
|
+
* pipeline the device starts answers it, and so does a yes/no button press. Answering it (by voice or button) closes
|
|
187
|
+
* it earlier. The wake word phrase cannot tell a follow-up from a new request: ESPHome sends the phrase of the last
|
|
188
|
+
* wake word with every request, the follow-up microphone included (field report 2026-11-05).
|
|
187
189
|
*/
|
|
188
|
-
const
|
|
190
|
+
const QUESTION_OPEN_MS = 30_000;
|
|
189
191
|
/**
|
|
190
192
|
* A button press still answers the question this long after the follow-up microphone closed: the firmware may stop
|
|
191
193
|
* the microphone on the press before it reports which button it was.
|
|
@@ -430,11 +432,15 @@ class EsphomeConnection {
|
|
|
430
432
|
loggedFormat = '';
|
|
431
433
|
/**
|
|
432
434
|
* The last reply asked something (`continue_conversation`, or an announcement with `start_conversation`): until
|
|
433
|
-
* when a yes/no button press answers it
|
|
435
|
+
* when the next request of the device, or a yes/no button press, answers it. 0 = no open question.
|
|
434
436
|
*/
|
|
435
437
|
questionUntil = 0;
|
|
436
438
|
/** After a button answer: until when a follow-up microphone is the stale one of the answered question. */
|
|
437
439
|
ignoreFollowUpUntil = 0;
|
|
440
|
+
/** When a yes/no button press last answered a question. */
|
|
441
|
+
buttonAt = 0;
|
|
442
|
+
/** When the device last asked to stop (`VoiceAssistantRequest{start:false}`). */
|
|
443
|
+
stoppedAt = 0;
|
|
438
444
|
/** The display language was compared with ours since the last connect. */
|
|
439
445
|
displayLanguageChecked = false;
|
|
440
446
|
constructor(device, opts) {
|
|
@@ -773,6 +779,10 @@ class EsphomeConnection {
|
|
|
773
779
|
return;
|
|
774
780
|
}
|
|
775
781
|
this.questionUntil = 0;
|
|
782
|
+
// Pressed while the question still played: the device may open the microphone for it afterwards. Not if the
|
|
783
|
+
// microphone is open already (our RUN_END closes it for good), and not if the device just stopped its
|
|
784
|
+
// conversation itself — the firmware does that on the press, which also drops its continue flag.
|
|
785
|
+
const micStillComes = !this.capture && Date.now() - this.stoppedAt >= CONFIRM_GRACE_MS;
|
|
776
786
|
if (this.capture) {
|
|
777
787
|
// A RUN_END while the microphone streams stops it and puts the device back to idle — without the error
|
|
778
788
|
// display an empty recognition would end with.
|
|
@@ -782,7 +792,8 @@ class EsphomeConnection {
|
|
|
782
792
|
// Whatever is still in flight for the question stays silent; so does the follow-up microphone the device may
|
|
783
793
|
// still open for it.
|
|
784
794
|
const run = ++this.run;
|
|
785
|
-
this.
|
|
795
|
+
this.buttonAt = Date.now();
|
|
796
|
+
this.ignoreFollowUpUntil = micStillComes ? Date.now() + IGNORE_FOLLOW_UP_MS : 0;
|
|
786
797
|
void this.answerButton(yes, run);
|
|
787
798
|
}
|
|
788
799
|
/** Pass a button answer on and announce the reply (see {@link onConfirmButton}). */
|
|
@@ -827,7 +838,11 @@ class EsphomeConnection {
|
|
|
827
838
|
.list()
|
|
828
839
|
.map(e => e.objectId)
|
|
829
840
|
.join(', ');
|
|
830
|
-
const options = entity?.options
|
|
841
|
+
const options = entity?.options
|
|
842
|
+
? ` (one of: ${entity.options.join(', ')})`
|
|
843
|
+
: entity?.kind === 'text'
|
|
844
|
+
? ` (${entity.minLength || 0}–${entity.maxLength ?? '∞'} bytes${entity.pattern ? `, pattern ${entity.pattern}` : ''})`
|
|
845
|
+
: '';
|
|
831
846
|
this.opts.log.warn(entity
|
|
832
847
|
? `ESPHome ${this.deviceName}: ${JSON.stringify(value)} is not a value "${objectId}" accepts${options}`
|
|
833
848
|
: `ESPHome ${this.deviceName}: no entity "${objectId}" (have: ${known})`);
|
|
@@ -859,7 +874,7 @@ class EsphomeConnection {
|
|
|
859
874
|
return;
|
|
860
875
|
}
|
|
861
876
|
const phrase = request.getWakeWordPhrase();
|
|
862
|
-
if (
|
|
877
|
+
if (Date.now() < this.ignoreFollowUpUntil) {
|
|
863
878
|
// The microphone for the question a button already answered: close it at once. In this state the device
|
|
864
879
|
// takes a RUN_END as "stop the microphone" and goes back to idle.
|
|
865
880
|
this.ignoreFollowUpUntil = 0;
|
|
@@ -873,13 +888,12 @@ class EsphomeConnection {
|
|
|
873
888
|
this.opts.log.debug(`ESPHome ${this.deviceName}: new wake while answering — dropping the old reply`);
|
|
874
889
|
}
|
|
875
890
|
this.run++;
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
this.opts.log.debug(`ESPHome ${this.deviceName}: wake "${phrase || '(follow-up)'}"`);
|
|
891
|
+
// While a question is open the next request answers it. The phrase says nothing here: the follow-up
|
|
892
|
+
// microphone carries the last wake word too. Should the user really have said the wake word, the utterance
|
|
893
|
+
// is still the next one after the question — which is what pi-agent and askUser take as the answer anyway.
|
|
894
|
+
const answersQuestion = Date.now() < this.questionUntil;
|
|
895
|
+
const followUp = answersQuestion || !phrase;
|
|
896
|
+
this.opts.log.debug(`ESPHome ${this.deviceName}: ${answersQuestion ? 'answer to the question' : followUp ? 'follow-up' : `wake "${phrase}"`}`);
|
|
883
897
|
const response = new esphomeProto_1.pb.VoiceAssistantResponse();
|
|
884
898
|
response.setPort(0); // 0 = stream the audio over this socket
|
|
885
899
|
response.setError(false);
|
|
@@ -903,13 +917,13 @@ class EsphomeConnection {
|
|
|
903
917
|
// travel: derive them from the room instead, once the wake chirp is out of the way.
|
|
904
918
|
skipMs: WAKE_CHIRP_MS,
|
|
905
919
|
adaptive: true,
|
|
906
|
-
startTimeoutMs:
|
|
920
|
+
startTimeoutMs: followUp ? NO_SPEECH_FOLLOW_UP_MS : NO_SPEECH_MS,
|
|
907
921
|
minStartLevel: this.speechLevel || exports.DEFAULT_SPEECH_LEVEL,
|
|
908
922
|
minEndLevel: Math.round((this.speechLevel || exports.DEFAULT_SPEECH_LEVEL) * SILENCE_SHARE),
|
|
909
923
|
minSpeechMs: MIN_SPEECH_MS,
|
|
910
924
|
}),
|
|
911
925
|
conversationId: request.getConversationId(),
|
|
912
|
-
wakeWord:
|
|
926
|
+
wakeWord: followUp ? '' : phrase,
|
|
913
927
|
answersQuestion,
|
|
914
928
|
timer,
|
|
915
929
|
stallCheck,
|
|
@@ -919,7 +933,7 @@ class EsphomeConnection {
|
|
|
919
933
|
};
|
|
920
934
|
this.status('listening');
|
|
921
935
|
this.event(EVENT.RUN_START);
|
|
922
|
-
if (
|
|
936
|
+
if (!followUp) {
|
|
923
937
|
this.event(EVENT.WAKE_WORD_END, { wake_word_phrase: phrase });
|
|
924
938
|
}
|
|
925
939
|
this.event(EVENT.STT_START);
|
|
@@ -932,9 +946,19 @@ class EsphomeConnection {
|
|
|
932
946
|
* every further wake word just sends another stop (field report 2026-11-03).
|
|
933
947
|
*/
|
|
934
948
|
onStopRequest() {
|
|
949
|
+
this.stoppedAt = Date.now();
|
|
935
950
|
const running = !!this.capture || this.runOpen;
|
|
936
|
-
|
|
937
|
-
|
|
951
|
+
if (!running && Date.now() - this.buttonAt < CONFIRM_GRACE_MS) {
|
|
952
|
+
// A device with yes/no buttons reports the press and then stops its conversation itself: the button
|
|
953
|
+
// answer ended our run already, and its reply is on the way — nothing to end, nothing to drop. With the
|
|
954
|
+
// stop the device dropped its continue flag too, so no microphone opens for the answered question.
|
|
955
|
+
this.ignoreFollowUpUntil = 0;
|
|
956
|
+
this.opts.log.debug(`ESPHome ${this.deviceName}: the device ended the conversation after the button`);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
// A device with yes/no buttons may also stop the microphone on the press before it reports which button it
|
|
960
|
+
// was (state messages can be batched): the question stays open a moment longer for that report.
|
|
961
|
+
this.questionUntil = this.questionOpen() ? Math.max(this.questionUntil, Date.now() + CONFIRM_GRACE_MS) : 0;
|
|
938
962
|
this.abortCapture();
|
|
939
963
|
if (running) {
|
|
940
964
|
this.run++; // whatever is still being answered stays silent
|
|
@@ -1037,7 +1061,9 @@ class EsphomeConnection {
|
|
|
1037
1061
|
if (capture.answersQuestion) {
|
|
1038
1062
|
// Spoken: that is the answer, a button press now would be a second one. Silent: the buttons still answer
|
|
1039
1063
|
// for a moment, as the screen may still show them.
|
|
1040
|
-
this.questionUntil = capture.detector.sawSpeech
|
|
1064
|
+
this.questionUntil = capture.detector.sawSpeech
|
|
1065
|
+
? 0
|
|
1066
|
+
: Math.max(this.questionUntil, Date.now() + CONFIRM_GRACE_MS);
|
|
1041
1067
|
}
|
|
1042
1068
|
if (!pcm.length || !capture.detector.sawSpeech) {
|
|
1043
1069
|
const loudest = Math.round(capture.detector.analysedPeak);
|
|
@@ -1083,7 +1109,7 @@ class EsphomeConnection {
|
|
|
1083
1109
|
this.opts.log.info(`Voice Q (${this.deviceName}): (empty) — speech recognition returned no text for ${seconds.toFixed(1)} s of audio`);
|
|
1084
1110
|
this.discard(capture, pcm, 'empty-stt', vad);
|
|
1085
1111
|
if (capture.answersQuestion) {
|
|
1086
|
-
this.questionUntil = Date.now() + CONFIRM_GRACE_MS;
|
|
1112
|
+
this.questionUntil = Math.max(this.questionUntil, Date.now() + CONFIRM_GRACE_MS);
|
|
1087
1113
|
}
|
|
1088
1114
|
// No STT_END without text — like Home Assistant, which reports this case as an error only.
|
|
1089
1115
|
this.endWithoutRequest();
|
|
@@ -1132,9 +1158,7 @@ class EsphomeConnection {
|
|
|
1132
1158
|
this.event(EVENT.TTS_END, { url });
|
|
1133
1159
|
// linux-voice-assistant reads the flag here (ignoring "0"), ESPHome at INTENT_END
|
|
1134
1160
|
this.event(EVENT.RUN_END, conversation);
|
|
1135
|
-
this.questionUntil = followUp
|
|
1136
|
-
? Date.now() + (ttsPcm.length / 2 / sampleRate) * 1000 + FOLLOW_UP_START_MS
|
|
1137
|
-
: 0;
|
|
1161
|
+
this.questionUntil = followUp ? Date.now() + (ttsPcm.length / 2 / sampleRate) * 1000 + QUESTION_OPEN_MS : 0;
|
|
1138
1162
|
this.armPlaybackTimeout();
|
|
1139
1163
|
}
|
|
1140
1164
|
catch (e) {
|
|
@@ -1251,7 +1275,7 @@ class EsphomeConnection {
|
|
|
1251
1275
|
if (startConversation) {
|
|
1252
1276
|
// A question: the microphone the device opens after it is wanted, and a button press answers it
|
|
1253
1277
|
this.ignoreFollowUpUntil = 0;
|
|
1254
|
-
this.questionUntil = Date.now() + (pcm.length / 2 / sampleRate) * 1000 +
|
|
1278
|
+
this.questionUntil = Date.now() + (pcm.length / 2 / sampleRate) * 1000 + QUESTION_OPEN_MS;
|
|
1255
1279
|
}
|
|
1256
1280
|
this.status('speaking');
|
|
1257
1281
|
this.send(request);
|