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.
@@ -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 device may take to play it and
186
- * open the microphone. Until then a button press answers the question.
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 FOLLOW_UP_START_MS = 10_000;
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 while no follow-up recording is open. 0 = no open question.
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.ignoreFollowUpUntil = Date.now() + IGNORE_FOLLOW_UP_MS;
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 ? ` (one of: ${entity.options.join(', ')})` : '';
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 (!phrase && Date.now() < this.ignoreFollowUpUntil) {
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
- // Without a wake word the device re-opened the mic for the reply that asked something; with one, the user
877
- // started over and the question is gone.
878
- const answersQuestion = !phrase && Date.now() < this.questionUntil;
879
- if (phrase) {
880
- this.questionUntil = 0;
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: phrase ? NO_SPEECH_MS : NO_SPEECH_FOLLOW_UP_MS,
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: phrase || '',
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 (phrase) {
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
- // A device with yes/no buttons may stop its microphone on the press before it reports which button it was.
937
- this.questionUntil = this.questionOpen() ? Date.now() + CONFIRM_GRACE_MS : 0;
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 ? 0 : Date.now() + CONFIRM_GRACE_MS;
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 + FOLLOW_UP_START_MS;
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);