Skip to content

Commit 25d363c

Browse files
committed
[UI] Add voice dialog
Signed-off-by: Miguel Álvarez Díez <[email protected]>
1 parent ea5121d commit 25d363c

File tree

17 files changed

+1541
-16
lines changed

17 files changed

+1541
-16
lines changed

bundles/org.openhab.ui/web/build/webpack.config.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,16 @@ module.exports = {
7070
allowedHosts: "all",
7171
historyApiFallback: true,
7272
proxy: [
73-
{
74-
context: ['/auth', '/rest', '/chart', '/proxy', '/icon', '/static', '/changePassword', '/createApiToken', '/audio'],
75-
target: apiBaseUrl
76-
},
77-
{
78-
context: ['/ws/logs', '/ws/events'],
79-
target: apiBaseUrl,
80-
ws: true
81-
}
82-
]
73+
{
74+
context: ['/auth', '/rest', '/chart', '/proxy', '/icon', '/static', '/changePassword', '/createApiToken', '/audio'],
75+
target: apiBaseUrl
76+
},
77+
{
78+
context: ['/ws/logs', '/ws/events', '/ws/audio-pcm'],
79+
target: apiBaseUrl,
80+
ws: true
81+
}
82+
]
8383
},
8484
performance: {
8585
maxAssetSize: 2048000,

bundles/org.openhab.ui/web/package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bundles/org.openhab.ui/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"path-browserify": "^1.0.1",
8585
"pkce-challenge": "^3.1.0",
8686
"qrcode": "^1.5.4",
87+
"reentrant-lock": "^3.0.0",
8788
"scope-css": "^1.2.1",
8889
"stream-browserify": "^3.0.0",
8990
"template7": "^1.4.2",

bundles/org.openhab.ui/web/src/assets/i18n/common/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"home.editHome": "Edit Home Page",
3434
"home.pinToHome": "Pin to Home",
3535
"home.exitToApp": "Return to App",
36+
"home.triggerVoice": "Trigger voice dialog",
3637
"home.tip.otherApps": "Open the apps panel to launch other interfaces",
3738
"sidebar.noPages": "No pages",
3839
"sidebar.administration": "Administration",

bundles/org.openhab.ui/web/src/assets/i18n/theme-switcher/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,10 @@
2020
"about.miscellaneous.theme.disablePageTransition": "Disable page transition animations",
2121
"about.miscellaneous.webaudio.enable": "Enable Web Audio sink support",
2222
"about.miscellaneous.commandItem.title": "Listen for UI commands to ",
23-
"about.miscellaneous.commandItem.selectItem": "Item"
23+
"about.miscellaneous.commandItem.selectItem": "Item",
24+
"about.dialog": "Voice Support",
25+
"about.dialog.enable": "Enable Voice Dialog",
26+
"about.dialog.id": "Connection Id",
27+
"about.dialog.listeningItem": "Listening Item",
28+
"about.dialog.locationItem": "Location Item"
2429
}

bundles/org.openhab.ui/web/src/components/app.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,15 @@ import auth from './auth-mixin'
276276
import i18n from './i18n-mixin'
277277
import connectionHealth from './connection-health-mixin'
278278
import sseEvents from './sse-events-mixin'
279+
import dialog from './dialog-mixin'
279280
280281
import dayjs from 'dayjs'
281282
import dayjsLocales from 'dayjs/locale.json'
282283
283284
import { AddonIcons, AddonTitles } from '@/assets/addon-store'
284285
285286
export default {
286-
mixins: [auth, i18n, connectionHealth, sseEvents],
287+
mixins: [auth, i18n, connectionHealth, sseEvents, dialog],
287288
components: {
288289
EmptyStatePlaceholder,
289290
PanelRight,
@@ -700,11 +701,16 @@ export default {
700701
}
701702
})
702703
704+
this.$f7.on('triggerDialog', () => {
705+
this.triggerDialog()
706+
})
707+
703708
if (window) {
704709
window.addEventListener('keydown', this.keyDown)
705710
}
706711
707712
this.startEventSource()
713+
this.startAudioWebSocket()
708714
})
709715
}
710716
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { getAccessToken } from '@/js/openhab/auth.js'
2+
3+
export default {
4+
data () {
5+
return {
6+
audioMain: null
7+
}
8+
},
9+
methods: {
10+
startAudioWebSocket () {
11+
if (this.audioMain) {
12+
return
13+
}
14+
const dialogEnabled = localStorage.getItem('openhab.ui:dialog.enabled') === 'true'
15+
if (!dialogEnabled) {
16+
return
17+
}
18+
const identifier = localStorage.getItem('openhab.ui:dialog.id') ?? 'anonymous'
19+
const dialogListeningItem = localStorage.getItem('openhab.ui:dialog.listeningItem') ?? ''
20+
const dialogLocationItem = localStorage.getItem('openhab.ui:dialog.locationItem') ?? ''
21+
import('../js/voice/audio-main.js').then(({ AudioMain }) => {
22+
if (this.audioMain) {
23+
return
24+
}
25+
let port = ''
26+
if (!((location.protocol === 'https:' && location.port === '443') || (location.protocol === 'http:' && location.port === '80'))) {
27+
port = `:${location.port}`
28+
}
29+
const ohURL = `${location.protocol}//${location.hostname}${port}`
30+
const updatePageIcon = (online, recording, playing) => {
31+
let voiceIcon
32+
if (!online) {
33+
voiceIcon = 'f7:mic_slash_fill'
34+
} else if (recording) {
35+
voiceIcon = 'f7:mic_circle_fill'
36+
} else if (playing) {
37+
voiceIcon = 'f7:speaker_2_fill'
38+
} else {
39+
voiceIcon = 'f7:mic_circle'
40+
}
41+
this.$store.commit('setVoiceIcon', voiceIcon)
42+
}
43+
updatePageIcon(false)
44+
this.audioMain = new AudioMain(ohURL, getAccessToken, {
45+
onMessage: (...args) => {
46+
console.debug('Voice: ' + args[0])
47+
},
48+
onRunningChange (io) {
49+
updatePageIcon(io.isRunning(), io.isListening(), io.isSpeaking())
50+
},
51+
onListeningChange (io) {
52+
updatePageIcon(io.isRunning(), io.isListening(), io.isSpeaking())
53+
},
54+
onSpeakingChange (io) {
55+
updatePageIcon(io.isRunning(), io.isListening(), io.isSpeaking())
56+
}
57+
})
58+
const events = ['touchstart', 'touchend', 'mousedown', 'keydown']
59+
const startAudio = () => {
60+
clean()
61+
this.audioMain.initialize(identifier, dialogListeningItem, dialogLocationItem)
62+
}
63+
const clean = () => events.forEach(e => document.body.removeEventListener(e, startAudio))
64+
events.forEach(e => document.body.addEventListener(e, startAudio, false))
65+
})
66+
},
67+
triggerDialog () {
68+
if (this.audioMain != null) {
69+
this.audioMain.sendSpot()
70+
}
71+
}
72+
}
73+
}

bundles/org.openhab.ui/web/src/components/theme-switcher.vue

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,26 @@
9898
</f7-list>
9999
</f7-col>
100100
</f7-row>
101+
102+
<f7-row v-if="showDialogOptions">
103+
<f7-col>
104+
<f7-block-title v-t="'about.dialog'" />
105+
<f7-list>
106+
<f7-list-item>
107+
<span v-t="'about.dialog.enable'" />
108+
<f7-toggle :checked="dialog === 'true'" @toggle:change="setDialog" />
109+
</f7-list-item>
110+
<f7-list-item>
111+
<span v-t="'about.dialog.id'" />
112+
<f7-input type="button" :value="identifier" />
113+
</f7-list-item>
114+
<item-picker :title="$t('about.dialog.listeningItem')" :multiple="false" :value="listeningItem"
115+
@input="setDialogListeningItem" />
116+
<item-picker :title="$t('about.dialog.locationItem')" :multiple="false" :value="locationItem"
117+
@input="setDialogLocationItem" />
118+
</f7-list>
119+
</f7-col>
120+
</f7-row>
101121
</f7-block>
102122
</template>
103123

@@ -111,7 +131,6 @@
111131
<script>
112132
import { loadLocaleMessages } from '@/js/i18n'
113133
import ItemPicker from '@/components/config/controls/item-picker.vue'
114-
115134
export default {
116135
components: {
117136
ItemPicker
@@ -165,6 +184,18 @@ export default {
165184
setCommandItem (value) {
166185
localStorage.setItem('openhab.ui:commandItem', value)
167186
setTimeout(() => { location.reload() }, 50) // Delay reload, otherwise it doesn't work
187+
},
188+
setDialog (value) {
189+
localStorage.setItem('openhab.ui:dialog.enabled', value)
190+
setTimeout(() => { location.reload() }, 50) // Delay reload, otherwise it doesn't work
191+
},
192+
setDialogListeningItem (value) {
193+
localStorage.setItem('openhab.ui:dialog.listeningItem', value)
194+
setTimeout(() => { location.reload() }, 50) // Delay reload, otherwise it doesn't work
195+
},
196+
setDialogLocationItem (value) {
197+
localStorage.setItem('openhab.ui:dialog.locationItem', value)
198+
setTimeout(() => { location.reload() }, 50) // Delay reload, otherwise it doesn't work
168199
}
169200
},
170201
computed: {
@@ -197,6 +228,30 @@ export default {
197228
},
198229
commandItem () {
199230
return localStorage.getItem('openhab.ui:commandItem') || ''
231+
},
232+
showDialogOptions () {
233+
const getUserMediaSupported = !!(window.navigator && window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia)
234+
return getUserMediaSupported &&
235+
!!window.AudioContext &&
236+
!!window.crypto
237+
},
238+
dialog () {
239+
return localStorage.getItem('openhab.ui:dialog.enabled') || 'default'
240+
},
241+
identifier () {
242+
const key = 'openhab.ui:dialog.id'
243+
let id = localStorage.getItem(key)
244+
if (!id) {
245+
id = `ui-${Math.round(Math.random() * 100)}-${Math.round(Math.random() * 100)}`
246+
localStorage.setItem(key, id)
247+
}
248+
return id
249+
},
250+
listeningItem () {
251+
return localStorage.getItem('openhab.ui:dialog.listeningItem') || ''
252+
},
253+
locationItem () {
254+
return localStorage.getItem('openhab.ui:dialog.locationItem') || ''
200255
}
201256
}
202257
}
@@ -284,5 +339,8 @@ export default {
284339
.nav-bars-picker-fill .demo-navbar:before,
285340
.nav-bars-picker-fill .demo-navbar:after
286341
background #fff
287-
342+
.title-fixed .item-title
343+
width: 200%
344+
.input-right input
345+
text-align: right
288346
</style>

bundles/org.openhab.ui/web/src/js/store/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ const store = new Vuex.Store({
3333
},
3434
websiteUrl: null,
3535
developerDock: false,
36-
pagePath: null
36+
pagePath: null,
37+
voiceIcon: null
3738
},
3839
getters: {
3940
apiEndpoint: (state) => (type) => (!state.apiEndpoints) ? null : state.apiEndpoints.find((e) => e.type === type),
40-
locale: (state, getters) => state.locale ?? 'default'
41+
locale: (state, getters) => state.locale ?? 'default',
42+
voiceIcon: (state) => state.voiceIcon
4143
},
4244
mutations: {
4345
setRootResource (state, { rootResponse }) {
@@ -56,6 +58,9 @@ const store = new Vuex.Store({
5658
},
5759
setPagePath (state, value) {
5860
state.pagePath = value
61+
},
62+
setVoiceIcon (state, value) {
63+
state.voiceIcon = value
5964
}
6065
},
6166
actions: {

0 commit comments

Comments
 (0)