iobroker.assistant 1.4.4 → 1.4.5
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 +7 -0
- package/admin/custom/.vite/manifest.json +1 -1
- package/admin/custom/assets/Components-BR57MZLJ.js +2 -0
- package/admin/custom/customComponents.js +1 -1
- package/admin/custom/i18n/de.json +3 -0
- package/admin/custom/i18n/en.json +3 -0
- package/admin/custom/i18n/es.json +3 -0
- package/admin/custom/i18n/fr.json +3 -0
- package/admin/custom/i18n/it.json +3 -0
- package/admin/custom/i18n/nl.json +3 -0
- package/admin/custom/i18n/pl.json +3 -0
- package/admin/custom/i18n/pt.json +3 -0
- package/admin/custom/i18n/ru.json +3 -0
- package/admin/custom/i18n/uk.json +3 -0
- package/admin/custom/i18n/zh-cn.json +3 -0
- 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/voice/esphome.js +160 -19
- package/build/lib/voice/esphome.js.map +1 -1
- package/build/lib/voice/esphomeEntities.js +76 -6
- package/build/lib/voice/esphomeEntities.js.map +1 -1
- package/build/lib/voice/esphomeProto.js.map +1 -1
- package/build/lib/voice/flac.js +162 -0
- package/build/lib/voice/flac.js.map +1 -0
- package/build/lib/voice/mediaServer.js +8 -1
- package/build/lib/voice/mediaServer.js.map +1 -1
- package/build/lib/voice/replyFormat.js +158 -0
- package/build/lib/voice/replyFormat.js.map +1 -0
- package/build/lib/voice/stt.js +2 -3
- package/build/lib/voice/stt.js.map +1 -1
- package/build/main.js +153 -3
- package/build/main.js.map +1 -1
- package/io-package.json +14 -14
- package/package.json +1 -1
- package/admin/custom/assets/Components-CCl7INIF.js +0 -2
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.EsphomeSatellites = exports.TIMER_EVENT = exports.DEFAULT_SPEECH_LEVEL = void 0;
|
|
37
37
|
exports.speechLevelOf = speechLevelOf;
|
|
38
|
+
exports.esphomeAddress = esphomeAddress;
|
|
38
39
|
/**
|
|
39
40
|
* ESPHome voice satellites — the fourth transport, next to the UDP server (`voiceServer.ts`) and the
|
|
40
41
|
* Wyoming endpoint (`wyoming.ts`).
|
|
@@ -63,7 +64,7 @@ const esphomeProto_1 = require("./esphomeProto");
|
|
|
63
64
|
const esphomeEntities_1 = require("./esphomeEntities");
|
|
64
65
|
const vad_1 = require("./vad");
|
|
65
66
|
const speechAudio_1 = require("./speechAudio");
|
|
66
|
-
const
|
|
67
|
+
const replyFormat_1 = require("./replyFormat");
|
|
67
68
|
const captures_1 = require("./captures");
|
|
68
69
|
/** `VoiceAssistantEvent` (api.proto) — the subset we emit. */
|
|
69
70
|
const EVENT = {
|
|
@@ -154,6 +155,16 @@ const STREAM_START_MS = 5000;
|
|
|
154
155
|
const STALL_CHECK_MS = 250;
|
|
155
156
|
/** Silence threshold as a share of the speech threshold — keeps the default pair 250/150. */
|
|
156
157
|
const SILENCE_SHARE = 0.6;
|
|
158
|
+
/**
|
|
159
|
+
* The firmware answers an ERROR event with a stop request of its own (`signal_stop_`). One arriving this soon after
|
|
160
|
+
* our error belongs to it and needs no reply — the run is already over.
|
|
161
|
+
*/
|
|
162
|
+
const ERROR_ECHO_MS = 2000;
|
|
163
|
+
/**
|
|
164
|
+
* What ends a run that produced no request. Home Assistant sends the same code when the speech recognition finds no
|
|
165
|
+
* text, and the Voice PE firmware deliberately plays no error sound for it.
|
|
166
|
+
*/
|
|
167
|
+
const NO_TEXT_ERROR = { code: 'stt-no-text-recognized', message: 'No text recognized' };
|
|
157
168
|
/**
|
|
158
169
|
* A dropped utterance whose loudest frame reached this share of the speech threshold was probably a voice
|
|
159
170
|
* that was too quiet, not an empty room — worth telling the user where to lower the threshold.
|
|
@@ -164,6 +175,12 @@ function speechLevelOf(device) {
|
|
|
164
175
|
const level = Math.round(Number(device.speechLevel));
|
|
165
176
|
return Number.isFinite(level) && level > 0 ? level : 0;
|
|
166
177
|
}
|
|
178
|
+
/** How a configured device is identified: `ip`, or `ip:port` when it does not use the default port. */
|
|
179
|
+
function esphomeAddress(device) {
|
|
180
|
+
const ip = (device.ip || '').trim().toLowerCase();
|
|
181
|
+
const port = Number(device.port) || DEFAULT_PORT;
|
|
182
|
+
return port === DEFAULT_PORT ? ip : `${ip}:${port}`;
|
|
183
|
+
}
|
|
167
184
|
/** `VoiceAssistantTimerEvent` in api.proto. */
|
|
168
185
|
exports.TIMER_EVENT = { started: 0, updated: 1, cancelled: 2, finished: 3 };
|
|
169
186
|
/**
|
|
@@ -293,13 +310,29 @@ class EsphomeConnection {
|
|
|
293
310
|
password;
|
|
294
311
|
/** Configured speech threshold, 0 = {@link DEFAULT_SPEECH_LEVEL}. */
|
|
295
312
|
speechLevel;
|
|
313
|
+
/** `ip`, or `ip:port` for another port than the default — how the adapter recognises the table row. */
|
|
314
|
+
address;
|
|
315
|
+
/** The device's friendly name, for entities without a name of their own. */
|
|
316
|
+
friendlyName = '';
|
|
317
|
+
/**
|
|
318
|
+
* Number of the current pipeline run. A run whose number is no longer current was ended — the device asked to
|
|
319
|
+
* stop, started over, or went away — and whatever STT, answer or TTS is still in flight for it stays silent.
|
|
320
|
+
*/
|
|
321
|
+
run = 0;
|
|
322
|
+
/** From RUN_START until we sent RUN_END: the device waits for that RUN_END. */
|
|
323
|
+
runOpen = false;
|
|
324
|
+
/** When we last sent an ERROR event (see {@link ERROR_ECHO_MS}). */
|
|
325
|
+
errorSentAt = 0;
|
|
326
|
+
/** The reply format last reported in the log, to log it again only when it changes. */
|
|
327
|
+
loggedFormat = '';
|
|
296
328
|
constructor(device, opts) {
|
|
297
329
|
this.opts = opts;
|
|
298
330
|
this.host = device.ip.trim();
|
|
299
|
-
this.port = device.port || DEFAULT_PORT;
|
|
331
|
+
this.port = Number(device.port) || DEFAULT_PORT;
|
|
300
332
|
this.room = (device.room || '').trim();
|
|
301
333
|
this.password = device.password || '';
|
|
302
334
|
this.speechLevel = speechLevelOf(device);
|
|
335
|
+
this.address = esphomeAddress(device);
|
|
303
336
|
this.deviceName = this.host;
|
|
304
337
|
}
|
|
305
338
|
// ── lifecycle ───────────────────────────────────────────────────────────────────────────────────
|
|
@@ -355,6 +388,9 @@ class EsphomeConnection {
|
|
|
355
388
|
onDisconnected() {
|
|
356
389
|
this.clearTimers();
|
|
357
390
|
this.abortCapture();
|
|
391
|
+
// A reply still being worked out must not land on the next connection, in the middle of another run.
|
|
392
|
+
this.run++;
|
|
393
|
+
this.runOpen = false;
|
|
358
394
|
if (this.online) {
|
|
359
395
|
this.opts.log.info(`ESPHome satellite offline: ${this.deviceName}`);
|
|
360
396
|
}
|
|
@@ -388,11 +424,11 @@ class EsphomeConnection {
|
|
|
388
424
|
setOffline() {
|
|
389
425
|
if (this.online) {
|
|
390
426
|
this.online = false;
|
|
391
|
-
this.opts.onStatus?.(this.deviceName, this.room, 'offline');
|
|
427
|
+
this.opts.onStatus?.(this.deviceName, this.room, 'offline', this.address);
|
|
392
428
|
}
|
|
393
429
|
}
|
|
394
430
|
status(state) {
|
|
395
|
-
this.opts.onStatus?.(this.deviceName, this.room, state);
|
|
431
|
+
this.opts.onStatus?.(this.deviceName, this.room, state, this.address);
|
|
396
432
|
}
|
|
397
433
|
// ── protocol plumbing ───────────────────────────────────────────────────────────────────────────
|
|
398
434
|
send(message) {
|
|
@@ -479,6 +515,7 @@ class EsphomeConnection {
|
|
|
479
515
|
case 'DeviceInfoResponse': {
|
|
480
516
|
const info = message;
|
|
481
517
|
this.deviceName = info.getName() || this.host;
|
|
518
|
+
this.friendlyName = info.getFriendlyName?.() || info.getName() || '';
|
|
482
519
|
this.online = true;
|
|
483
520
|
this.opts.log.info(`ESPHome satellite online: ${this.deviceName} (${info.getModel()}) @ ${this.host}${this.room ? `, room ${this.room}` : ''}`);
|
|
484
521
|
this.status('idle');
|
|
@@ -556,7 +593,7 @@ class EsphomeConnection {
|
|
|
556
593
|
}
|
|
557
594
|
default:
|
|
558
595
|
if ((0, esphomeEntities_1.isListEntityMessage)(name)) {
|
|
559
|
-
this.entities.addFromAnnouncement(name, message);
|
|
596
|
+
this.entities.addFromAnnouncement(name, message, this.friendlyName);
|
|
560
597
|
}
|
|
561
598
|
else if ((0, esphomeEntities_1.isEntityStateMessage)(name)) {
|
|
562
599
|
const entity = this.entities.applyState(name, message);
|
|
@@ -600,13 +637,19 @@ class EsphomeConnection {
|
|
|
600
637
|
// ── voice pipeline ──────────────────────────────────────────────────────────────────────────────
|
|
601
638
|
onPipelineRequest(request) {
|
|
602
639
|
if (!request.getStart()) {
|
|
603
|
-
this.
|
|
640
|
+
this.onStopRequest();
|
|
604
641
|
return;
|
|
605
642
|
}
|
|
606
643
|
if (this.capture) {
|
|
607
644
|
this.opts.log.debug(`ESPHome ${this.deviceName}: pipeline already running — ignoring wake`);
|
|
608
645
|
return;
|
|
609
646
|
}
|
|
647
|
+
if (this.runOpen) {
|
|
648
|
+
// The device started over while the last request was still being answered: that answer is not
|
|
649
|
+
// wanted any more, and its events would land in the middle of the new run.
|
|
650
|
+
this.opts.log.debug(`ESPHome ${this.deviceName}: new wake while answering — dropping the old reply`);
|
|
651
|
+
}
|
|
652
|
+
this.run++;
|
|
610
653
|
const phrase = request.getWakeWordPhrase();
|
|
611
654
|
this.opts.log.debug(`ESPHome ${this.deviceName}: wake "${phrase || '(follow-up)'}"`);
|
|
612
655
|
const response = new esphomeProto_1.pb.VoiceAssistantResponse();
|
|
@@ -653,6 +696,29 @@ class EsphomeConnection {
|
|
|
653
696
|
this.event(EVENT.STT_START);
|
|
654
697
|
this.event(EVENT.STT_VAD_START);
|
|
655
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* The device asks to end the conversation (`VoiceAssistantRequest{start:false}`): a button, or the wake word
|
|
701
|
+
* said again while a request is running — that is how ESPHome's voice_assistant stops. While it waits for an
|
|
702
|
+
* answer, the firmware leaves that state only on our RUN_END; without one it stays "in the conversation", and
|
|
703
|
+
* every further wake word just sends another stop (field report 2026-11-03).
|
|
704
|
+
*/
|
|
705
|
+
onStopRequest() {
|
|
706
|
+
const running = !!this.capture || this.runOpen;
|
|
707
|
+
this.abortCapture();
|
|
708
|
+
if (running) {
|
|
709
|
+
this.run++; // whatever is still being answered stays silent
|
|
710
|
+
this.opts.log.debug(`ESPHome ${this.deviceName}: the device ended the conversation`);
|
|
711
|
+
this.event(EVENT.RUN_END);
|
|
712
|
+
this.status('idle');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (Date.now() - this.errorSentAt < ERROR_ECHO_MS) {
|
|
716
|
+
return; // the device's own reaction to our ERROR event
|
|
717
|
+
}
|
|
718
|
+
// Nothing is open on our side, but the device still thinks so (it missed a RUN_END): end it there too.
|
|
719
|
+
this.opts.log.debug(`ESPHome ${this.deviceName}: stop outside a conversation — ending it on the device`);
|
|
720
|
+
this.event(EVENT.RUN_END);
|
|
721
|
+
}
|
|
656
722
|
onAudio(audio) {
|
|
657
723
|
const capture = this.capture;
|
|
658
724
|
if (!capture || capture.closed) {
|
|
@@ -718,6 +784,15 @@ class EsphomeConnection {
|
|
|
718
784
|
this.capture = null;
|
|
719
785
|
clearTimeout(capture.timer);
|
|
720
786
|
clearInterval(capture.stallCheck);
|
|
787
|
+
const run = this.run;
|
|
788
|
+
/** False once the run was ended elsewhere — then nothing more may be sent for it. */
|
|
789
|
+
const current = () => {
|
|
790
|
+
if (run === this.run) {
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
this.opts.log.debug(`ESPHome ${this.deviceName}: conversation ended meanwhile — dropping the reply`);
|
|
794
|
+
return false;
|
|
795
|
+
};
|
|
721
796
|
// Send this first: the device streams until it sees it.
|
|
722
797
|
this.event(EVENT.STT_VAD_END);
|
|
723
798
|
const pcm = Buffer.concat(capture.chunks);
|
|
@@ -747,13 +822,15 @@ class EsphomeConnection {
|
|
|
747
822
|
this.opts.log.debug(`ESPHome ${this.deviceName}: nothing but silence${end === 'no-speech' ? ' (nobody started speaking)' : ''} — dropping the utterance`);
|
|
748
823
|
}
|
|
749
824
|
this.discard(capture, pcm, end === 'no-speech' ? 'no-speech' : 'silence', vad);
|
|
750
|
-
this.
|
|
751
|
-
this.status('idle');
|
|
825
|
+
this.endWithoutRequest();
|
|
752
826
|
return;
|
|
753
827
|
}
|
|
754
828
|
this.status('processing');
|
|
755
829
|
try {
|
|
756
830
|
const hints = this.opts.getHints ? await this.opts.getHints() : undefined;
|
|
831
|
+
if (!current()) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
757
834
|
const speech = (0, speechAudio_1.prepareForStt)(pcm, MIC_SAMPLE_RATE, { startMs: vad.speechStartMs, endLevel: vad.endLevel });
|
|
758
835
|
vad.stt = { fromMs: speech.fromMs, toMs: speech.toMs, gain: speech.gain };
|
|
759
836
|
this.opts.log.debug(`ESPHome ${this.deviceName}: speech recognition gets ${(speech.fromMs / 1000).toFixed(2)}–` +
|
|
@@ -761,16 +838,19 @@ class EsphomeConnection {
|
|
|
761
838
|
const sttStart = Date.now();
|
|
762
839
|
const text = (await this.opts.stt.transcribe(speech.pcm, MIC_SAMPLE_RATE, this.opts.language, hints)).trim();
|
|
763
840
|
const sttMs = Date.now() - sttStart;
|
|
764
|
-
|
|
841
|
+
if (!current()) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
765
844
|
if (!text) {
|
|
766
845
|
// Speech was heard, so the user waits for an answer: say why none comes. Typically a word in another
|
|
767
846
|
// language than the STT listens in (a German "Ja" to a Russian STT comes back empty).
|
|
768
847
|
this.opts.log.info(`Voice Q (${this.deviceName}): (empty) — speech recognition returned no text for ${seconds.toFixed(1)} s of audio`);
|
|
769
848
|
this.discard(capture, pcm, 'empty-stt', vad);
|
|
770
|
-
this.
|
|
771
|
-
this.
|
|
849
|
+
// No STT_END without text — like Home Assistant, which reports this case as an error only.
|
|
850
|
+
this.endWithoutRequest();
|
|
772
851
|
return;
|
|
773
852
|
}
|
|
853
|
+
this.event(EVENT.STT_END, { text });
|
|
774
854
|
this.event(EVENT.INTENT_START);
|
|
775
855
|
const answer = (await this.opts.answer(text, {
|
|
776
856
|
device: this.deviceName,
|
|
@@ -781,6 +861,9 @@ class EsphomeConnection {
|
|
|
781
861
|
// Numbers only, no audio: shows where a request was cut off without keeping recordings.
|
|
782
862
|
vad: { ...vad, levels: (0, captures_1.levelProfile)(pcm, MIC_SAMPLE_RATE) },
|
|
783
863
|
})).trim();
|
|
864
|
+
if (!current()) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
784
867
|
const followUp = !!answer && this.opts.expectsFollowUp?.(answer, this.deviceName) === true;
|
|
785
868
|
this.event(EVENT.INTENT_END, followUp ? { continue_conversation: '1' } : undefined);
|
|
786
869
|
if (!answer) {
|
|
@@ -790,11 +873,17 @@ class EsphomeConnection {
|
|
|
790
873
|
}
|
|
791
874
|
const ttsStart = Date.now();
|
|
792
875
|
const { pcm: ttsPcm, sampleRate } = await this.opts.tts.synthesize(answer, this.opts.language);
|
|
876
|
+
if (!current()) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
793
879
|
this.opts.onSpoken?.(this.deviceName, {
|
|
794
880
|
ms: Date.now() - ttsStart,
|
|
795
881
|
audioMs: (ttsPcm.length / 2 / sampleRate) * 1000,
|
|
796
882
|
});
|
|
797
|
-
const url = this.publish(ttsPcm, sampleRate);
|
|
883
|
+
const url = await this.publish(ttsPcm, sampleRate);
|
|
884
|
+
if (!current()) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
798
887
|
this.status('speaking');
|
|
799
888
|
this.event(EVENT.TTS_START, { text: answer });
|
|
800
889
|
this.event(EVENT.TTS_END, { url });
|
|
@@ -804,12 +893,26 @@ class EsphomeConnection {
|
|
|
804
893
|
catch (e) {
|
|
805
894
|
const reason = e.message;
|
|
806
895
|
this.opts.log.error(`ESPHome ${this.deviceName}: voice pipeline failed — ${reason}`);
|
|
896
|
+
if (run !== this.run) {
|
|
897
|
+
return; // the conversation is over already — nothing to tell the device
|
|
898
|
+
}
|
|
807
899
|
// Without this the device stays ducked and its LED keeps spinning.
|
|
808
900
|
this.event(EVENT.ERROR, { code: 'pipeline-failed', message: reason });
|
|
809
901
|
this.event(EVENT.RUN_END);
|
|
810
902
|
this.status('idle');
|
|
811
903
|
}
|
|
812
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* End a run that produced no request. A RUN_END right after STT_VAD_END gets lost: the firmware is still stopping
|
|
907
|
+
* its microphone then, ignores RUN_END in that state, and afterwards waits for an answer that never comes — the
|
|
908
|
+
* satellite stays in the conversation (field report 2026-11-03). An ERROR event puts it back to idle in any state,
|
|
909
|
+
* so it goes first, with the code Home Assistant uses for this case.
|
|
910
|
+
*/
|
|
911
|
+
endWithoutRequest() {
|
|
912
|
+
this.event(EVENT.ERROR, NO_TEXT_ERROR);
|
|
913
|
+
this.event(EVENT.RUN_END);
|
|
914
|
+
this.status('idle');
|
|
915
|
+
}
|
|
813
916
|
/**
|
|
814
917
|
* Select which wake words listen on this device. Unknown ids are dropped and the list is trimmed
|
|
815
918
|
* to what the firmware allows, because the device answers a rejected list with silence rather than
|
|
@@ -886,12 +989,12 @@ class EsphomeConnection {
|
|
|
886
989
|
* Speak a ready PCM buffer on this device (timers, alarms, `tts.text`, jingles). With
|
|
887
990
|
* `startConversation` the microphone opens once the clip has played, to catch the answer to a question.
|
|
888
991
|
*/
|
|
889
|
-
announce(pcm, sampleRate, startConversation = false) {
|
|
992
|
+
async announce(pcm, sampleRate, startConversation = false) {
|
|
890
993
|
if (!this.online || !pcm.length) {
|
|
891
|
-
return
|
|
994
|
+
return;
|
|
892
995
|
}
|
|
893
996
|
const request = new esphomeProto_1.pb.VoiceAssistantAnnounceRequest();
|
|
894
|
-
request.setMediaId(this.publish(pcm, sampleRate));
|
|
997
|
+
request.setMediaId(await this.publish(pcm, sampleRate));
|
|
895
998
|
request.setText('');
|
|
896
999
|
request.setPreannounceMediaId('');
|
|
897
1000
|
// Needs feature flag 32 (START_CONVERSATION); on a device without it the clip still plays and
|
|
@@ -900,16 +1003,45 @@ class EsphomeConnection {
|
|
|
900
1003
|
this.status('speaking');
|
|
901
1004
|
this.send(request);
|
|
902
1005
|
this.armPlaybackTimeout();
|
|
903
|
-
|
|
1006
|
+
}
|
|
1007
|
+
/** The format replies should have on this device, from what its media player announced. */
|
|
1008
|
+
replyFormat() {
|
|
1009
|
+
const formats = this.entities
|
|
1010
|
+
.list()
|
|
1011
|
+
.filter(e => e.kind === 'mediaPlayer')
|
|
1012
|
+
.flatMap(e => e.formats ?? []);
|
|
1013
|
+
return (0, replyFormat_1.chooseReplyFormat)(formats, !!this.opts.encodeAudio);
|
|
904
1014
|
}
|
|
905
1015
|
/**
|
|
906
|
-
* Publish PCM
|
|
1016
|
+
* Publish PCM on the media server, in the format the device plays, and build the URL it should fetch.
|
|
907
1017
|
*
|
|
908
1018
|
* The host matters: on a multi-homed ioBroker machine the device can usually only reach one of the
|
|
909
1019
|
* addresses, and the local end of *this* socket is by definition one it can talk to.
|
|
910
1020
|
*/
|
|
911
|
-
publish(pcm, sampleRate) {
|
|
912
|
-
|
|
1021
|
+
async publish(pcm, sampleRate) {
|
|
1022
|
+
let format = this.replyFormat();
|
|
1023
|
+
let encoded;
|
|
1024
|
+
try {
|
|
1025
|
+
encoded = await (0, replyFormat_1.encodeReply)(pcm, sampleRate, format, this.opts.encodeAudio);
|
|
1026
|
+
}
|
|
1027
|
+
catch (e) {
|
|
1028
|
+
// MP3/Opus need ffmpeg. Without it a native format is the only chance, whether the device lists it or not.
|
|
1029
|
+
const fallback = { ...format, codec: 'flac', source: `${format.source}, FLAC as fallback` };
|
|
1030
|
+
this.opts.log.warn(`ESPHome ${this.deviceName}: cannot write the reply as ${format.codec.toUpperCase()} ` +
|
|
1031
|
+
`(${e.message}) — sending FLAC, which the device may not play. Install ffmpeg.`);
|
|
1032
|
+
format = fallback;
|
|
1033
|
+
encoded = await (0, replyFormat_1.encodeReply)(pcm, sampleRate, format);
|
|
1034
|
+
}
|
|
1035
|
+
const key = `${format.codec}/${format.sampleRate}/${format.channels}`;
|
|
1036
|
+
if (key !== this.loggedFormat) {
|
|
1037
|
+
this.loggedFormat = key;
|
|
1038
|
+
const what = !format.source
|
|
1039
|
+
? 'WAV (the device lists no formats)'
|
|
1040
|
+
: `${format.codec.toUpperCase()}${format.sampleRate ? ` ${format.sampleRate} Hz` : ''}` +
|
|
1041
|
+
`${format.channels === 2 ? ' stereo' : ''} (device: ${format.source})`;
|
|
1042
|
+
this.opts.log.info(`ESPHome ${this.deviceName}: spoken replies as ${what}`);
|
|
1043
|
+
}
|
|
1044
|
+
return this.mediaUrl(this.opts.media.publish(encoded.body, encoded.contentType));
|
|
913
1045
|
}
|
|
914
1046
|
/** Absolute URL of a path on the media server, with a host this device can reach. */
|
|
915
1047
|
mediaUrl(path) {
|
|
@@ -931,6 +1063,15 @@ class EsphomeConnection {
|
|
|
931
1063
|
this.playbackTimer.unref();
|
|
932
1064
|
}
|
|
933
1065
|
event(type, data) {
|
|
1066
|
+
if (type === EVENT.RUN_START) {
|
|
1067
|
+
this.runOpen = true;
|
|
1068
|
+
}
|
|
1069
|
+
else if (type === EVENT.RUN_END) {
|
|
1070
|
+
this.runOpen = false;
|
|
1071
|
+
}
|
|
1072
|
+
else if (type === EVENT.ERROR) {
|
|
1073
|
+
this.errorSentAt = Date.now();
|
|
1074
|
+
}
|
|
934
1075
|
const message = new esphomeProto_1.pb.VoiceAssistantEventResponse();
|
|
935
1076
|
message.setEventType(type);
|
|
936
1077
|
const items = [];
|