mirror of
https://github.com/Mesteriis/hassio-addons-avm.git
synced 2026-05-22 15:31:42 +02:00
add new
This commit is contained in:
108
google-assistant-webserver/app/assistant.py
Executable file
108
google-assistant-webserver/app/assistant.py
Executable file
@@ -0,0 +1,108 @@
|
||||
"""Google Assistant Text Assistant."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import google.auth.transport.grpc
|
||||
import google.auth.transport.requests
|
||||
import google.oauth2.credentials
|
||||
from aiohttp import web
|
||||
from google.assistant.embedded.v1alpha2 import (embedded_assistant_pb2,
|
||||
embedded_assistant_pb2_grpc)
|
||||
|
||||
import assistant_helpers
|
||||
|
||||
|
||||
ASSISTANT_API_ENDPOINT = 'embeddedassistant.googleapis.com'
|
||||
DEFAULT_GRPC_DEADLINE = 60 * 3 + 5
|
||||
PLAYING = embedded_assistant_pb2.ScreenOutConfig.PLAYING
|
||||
|
||||
|
||||
class GoogleTextAssistant(object):
|
||||
"""Sample Assistant that supports text based conversations.
|
||||
|
||||
Args:
|
||||
language_code: language for the conversation.
|
||||
device_model_id: identifier of the device model.
|
||||
device_id: identifier of the registered device instance.
|
||||
display: enable visual display of assistant response.
|
||||
cred_json: Filename of jsonfile containing credentials.
|
||||
deadline_sec: gRPC deadline in seconds for Google Assistant API call.
|
||||
"""
|
||||
|
||||
def __init__(self, language_code, device_model_id, device_id,
|
||||
cred_json:Path, display = True, deadline_sec = DEFAULT_GRPC_DEADLINE):
|
||||
self.language_code = language_code
|
||||
self.device_model_id = device_model_id
|
||||
self.device_id = device_id
|
||||
self.conversation_state = None
|
||||
# Force reset of first conversation.
|
||||
self.is_new_conversation = True
|
||||
self.display = display
|
||||
# open credentials
|
||||
with open(cred_json, 'r') as _file:
|
||||
credentials = google.oauth2.credentials.Credentials(token=None, **json.load(_file))
|
||||
http_request = google.auth.transport.requests.Request()
|
||||
credentials.refresh(http_request)
|
||||
# Create an authorized gRPC channel.
|
||||
grpc_channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, http_request, ASSISTANT_API_ENDPOINT)
|
||||
self.assistant = embedded_assistant_pb2_grpc.EmbeddedAssistantStub(
|
||||
grpc_channel
|
||||
)
|
||||
self.deadline = deadline_sec
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, etype, e, traceback):
|
||||
if e:
|
||||
return False
|
||||
|
||||
def assist(self, text_query):
|
||||
"""Send a text request to the Assistant and playback the response.
|
||||
"""
|
||||
def iter_assist_requests():
|
||||
config = embedded_assistant_pb2.AssistConfig(
|
||||
audio_out_config=embedded_assistant_pb2.AudioOutConfig(
|
||||
encoding='LINEAR16',
|
||||
sample_rate_hertz=16000,
|
||||
volume_percentage=0,
|
||||
),
|
||||
dialog_state_in=embedded_assistant_pb2.DialogStateIn(
|
||||
language_code=self.language_code,
|
||||
conversation_state=self.conversation_state,
|
||||
is_new_conversation=self.is_new_conversation,
|
||||
),
|
||||
device_config=embedded_assistant_pb2.DeviceConfig(
|
||||
device_id=self.device_id,
|
||||
device_model_id=self.device_model_id,
|
||||
),
|
||||
text_query=text_query,
|
||||
)
|
||||
# Continue current conversation with later requests.
|
||||
self.is_new_conversation = False
|
||||
if self.display:
|
||||
config.screen_out_config.screen_mode = PLAYING
|
||||
req = embedded_assistant_pb2.AssistRequest(config=config)
|
||||
# This can be used to output the assistant request
|
||||
# assistant_helpers.log_assist_request_without_audio(req)
|
||||
yield req
|
||||
|
||||
text_response = None
|
||||
html_response = None
|
||||
for resp in self.assistant.Assist(iter_assist_requests(),
|
||||
self.deadline):
|
||||
# This can be used to output the assistant response
|
||||
# assistant_helpers.log_assist_response_without_audio(resp)
|
||||
if resp.screen_out.data:
|
||||
html_response = resp.screen_out.data
|
||||
if resp.dialog_state_out.conversation_state:
|
||||
conversation_state = resp.dialog_state_out.conversation_state
|
||||
self.conversation_state = conversation_state
|
||||
if resp.dialog_state_out.supplemental_display_text:
|
||||
text_response = resp.dialog_state_out.supplemental_display_text()
|
||||
return text_response, html_response
|
||||
|
||||
54
google-assistant-webserver/app/assistant_helpers.py
Normal file
54
google-assistant-webserver/app/assistant_helpers.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helper functions for the Google Assistant API."""
|
||||
|
||||
import logging
|
||||
|
||||
from google.assistant.embedded.v1alpha2 import embedded_assistant_pb2
|
||||
|
||||
|
||||
def log_assist_request_without_audio(assist_request):
|
||||
"""Log AssistRequest fields without audio data."""
|
||||
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||
resp_copy = embedded_assistant_pb2.AssistRequest()
|
||||
resp_copy.CopyFrom(assist_request)
|
||||
if len(resp_copy.audio_in) > 0:
|
||||
size = len(resp_copy.audio_in)
|
||||
resp_copy.ClearField('audio_in')
|
||||
logging.debug('AssistRequest: audio_in (%d bytes)',
|
||||
size)
|
||||
return
|
||||
logging.debug('AssistRequest: %s', resp_copy)
|
||||
|
||||
|
||||
def log_assist_response_without_audio(assist_response):
|
||||
"""Log AssistResponse fields without audio data."""
|
||||
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||
resp_copy = embedded_assistant_pb2.AssistResponse()
|
||||
resp_copy.CopyFrom(assist_response)
|
||||
has_audio_data = (resp_copy.HasField('audio_out') and
|
||||
len(resp_copy.audio_out.audio_data) > 0)
|
||||
if has_audio_data:
|
||||
size = len(resp_copy.audio_out.audio_data)
|
||||
resp_copy.audio_out.ClearField('audio_data')
|
||||
if resp_copy.audio_out.ListFields():
|
||||
logging.debug('AssistResponse: %s audio_data (%d bytes)',
|
||||
resp_copy,
|
||||
size)
|
||||
else:
|
||||
logging.debug('AssistResponse: audio_data (%d bytes)',
|
||||
size)
|
||||
return
|
||||
logging.debug('AssistResponse: %s', resp_copy)
|
||||
89
google-assistant-webserver/app/auth.html
Normal file
89
google-assistant-webserver/app/auth.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<v-app>
|
||||
<v-main>
|
||||
<v-container fluid fill-height>
|
||||
<v-layout align-center justify-center>
|
||||
<v-flex sm8>
|
||||
<v-card class="elevation-12">
|
||||
<v-toolbar dark color="black">
|
||||
<v-toolbar-title>Authenticate Google Assistant Webserver</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<h2>Step 1</h2>
|
||||
<span>In order to use the Google Assistant webserver, you need to authenticate your Google account (once).</span>
|
||||
<span>Please click the button below to start the authentication process.</span>
|
||||
<span>Once you received the authentication token, come back to this page to submit it.</span>
|
||||
<br /><br />
|
||||
<v-btn @click="this.window.open('[[AUTH_URL]]', '_blank');" class="mr-4">Authenticate</v-btn>
|
||||
<br /><br />
|
||||
<h2>Step 2</h2>
|
||||
<span>Once you received the token, paste it below and click submit.</span>
|
||||
|
||||
<v-form ref="form" v-model="valid" method="post">
|
||||
<v-text-field
|
||||
v-model="token"
|
||||
prepend-icon="mdi-lock"
|
||||
name="token"
|
||||
label="Token"
|
||||
type="text"
|
||||
required
|
||||
:rules="[v => !!v || 'Token is required']"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-alert type="success" v-if="success"
|
||||
>Authentication finished. You can now issue commands to Google Assistant.
|
||||
</v-alert>
|
||||
<v-btn type="submit" v-else :disabled="!valid" color="success" @click="submit"
|
||||
class="mr-4">Submit</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
vuetify: new Vuetify(),
|
||||
data: () => ({
|
||||
token: '',
|
||||
valid: true,
|
||||
success: false
|
||||
}),
|
||||
methods: {
|
||||
submit () {
|
||||
const formData = new FormData()
|
||||
formData.append('token', this.token)
|
||||
axios.post(window.location + 'token', formData)
|
||||
.then(function (response) {
|
||||
this.success = true
|
||||
}.bind(this))
|
||||
.catch(function (error) {
|
||||
console.log('error', error)
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
61
google-assistant-webserver/app/auth.py
Normal file
61
google-assistant-webserver/app/auth.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Handler for authentication."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from aiohttp import web
|
||||
from google.oauth2.credentials import Credentials
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
|
||||
|
||||
class AuthHandler:
|
||||
"""Some logic to handle granting access to Google."""
|
||||
|
||||
def __init__(self, user_data:dict, cred_file:Path):
|
||||
"""Initialize."""
|
||||
self.cred_file = cred_file
|
||||
self.user_data = user_data
|
||||
|
||||
self.oauth2 = OAuth2Session(
|
||||
self.user_data["client_id"],
|
||||
redirect_uri="urn:ietf:wg:oauth:2.0:oob",
|
||||
scope="https://www.googleapis.com/auth/assistant-sdk-prototype",
|
||||
)
|
||||
self.auth_url, _ = self.oauth2.authorization_url(
|
||||
self.user_data["auth_uri"], access_type="offline", prompt="consent"
|
||||
)
|
||||
|
||||
async def token(self, request):
|
||||
"""Read access token and process it."""
|
||||
form = await request.post()
|
||||
token = form["token"]
|
||||
self.oauth2.fetch_token(
|
||||
self.user_data["token_uri"], client_secret=self.user_data["client_secret"], code=token
|
||||
)
|
||||
|
||||
# create credentials
|
||||
credentials = Credentials(
|
||||
self.oauth2.token["access_token"],
|
||||
refresh_token=self.oauth2.token.get("refresh_token"),
|
||||
token_uri=self.user_data["token_uri"],
|
||||
client_id=self.user_data["client_id"],
|
||||
client_secret=self.user_data["client_secret"],
|
||||
scopes=self.oauth2.scope,
|
||||
)
|
||||
|
||||
# write credentials json file
|
||||
with self.cred_file.open("w") as json_file:
|
||||
json_file.write(
|
||||
json.dumps(
|
||||
{
|
||||
"refresh_token": credentials.refresh_token,
|
||||
"token_uri": credentials.token_uri,
|
||||
"client_id": credentials.client_id,
|
||||
"client_secret": credentials.client_secret,
|
||||
"scopes": [credentials.scopes],
|
||||
}
|
||||
)
|
||||
)
|
||||
return web.Response(text="Authentication successfull")
|
||||
1
google-assistant-webserver/app/client_secrets.json
Executable file
1
google-assistant-webserver/app/client_secrets.json
Executable file
@@ -0,0 +1 @@
|
||||
{"installed":{"client_id":"848329555010-ue91trunkjk1dk48s6dov8csgthqnu54.apps.googleusercontent.com","project_id":"hass-assistant-229621","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"HHrDEJXTLgKfdUai-i7Icsws","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
|
||||
78
google-assistant-webserver/app/index.html
Normal file
78
google-assistant-webserver/app/index.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<v-app>
|
||||
<v-main>
|
||||
<v-container fluid fill-height>
|
||||
<v-layout align-center justify-center>
|
||||
<v-flex sm8>
|
||||
<v-card class="elevation-12">
|
||||
<v-toolbar dark color="black">
|
||||
<v-toolbar-title>Google Assistant Webserver</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<h2>Broadcast</h2>
|
||||
<v-text-field
|
||||
v-model="broadcast"
|
||||
label="Broadcast message"
|
||||
type="text"
|
||||
></v-text-field>
|
||||
<v-btn @click="submitBroadcast()" class="mr-4">Send broadcast</v-btn>
|
||||
<br />
|
||||
<br />
|
||||
<h2>Send command</h2>
|
||||
<v-text-field
|
||||
v-model="command"
|
||||
label="Command to send to Google Assistant (e.g. turn off the lights)"
|
||||
type="text"
|
||||
></v-text-field>
|
||||
<v-btn @click="submitCommand()" class="mr-4">Send command</v-btn>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<a @click="openAuth()">Authenticate</a>
|
||||
</v-card-actions>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
vuetify: new Vuetify(),
|
||||
data: () => ({
|
||||
broadcast: '',
|
||||
command: ''
|
||||
}),
|
||||
methods: {
|
||||
async submitBroadcast () {
|
||||
const res = await axios.get(window.location + 'broadcast', { params: { message: this.broadcast } })
|
||||
alert('Message submitted ' + res.data)
|
||||
},
|
||||
async submitCommand () {
|
||||
const res = await axios.get(window.location + 'command', { params: { message: this.command } })
|
||||
alert('Message submitted ' + res.data)
|
||||
},
|
||||
openAuth () {
|
||||
window.open(window.location + 'auth')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
67
google-assistant-webserver/app/main.py
Normal file
67
google-assistant-webserver/app/main.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Main entrypoint: webserver handling commands to google assistant."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from assistant import GoogleTextAssistant
|
||||
from auth import AuthHandler
|
||||
|
||||
LOGGER = logging.getLogger()
|
||||
|
||||
CLIENT_JSON = Path(os.path.join(os.path.dirname(os.path.abspath(__file__)), "client_secrets.json"))
|
||||
CRED_JSON = Path("/data/cred.json")
|
||||
if not os.path.isdir("/data"):
|
||||
CRED_JSON = Path(os.path.join(os.path.dirname(os.path.abspath(__file__)), "cred.json"))
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
|
||||
|
||||
@routes.get("/broadcast")
|
||||
async def broadcast_message(request):
|
||||
message = request.query.get("message", default="This is a test!")
|
||||
text_query = "broadcast " + message
|
||||
with GoogleTextAssistant("en-US", "HA_GA", "HA_GA_TEXT_SERVER", CRED_JSON) as assistant:
|
||||
response_text, response_html = assistant.assist(text_query=text_query)
|
||||
return web.Response(text=response_text)
|
||||
|
||||
|
||||
@routes.get("/command")
|
||||
async def command(request):
|
||||
message = request.query.get("message", default="This is a test!")
|
||||
with GoogleTextAssistant("en-US", "HA_GA", "HA_GA_TEXT_SERVER", CRED_JSON) as assistant:
|
||||
response_text, response_html = assistant.assist(text_query=message)
|
||||
return web.Response(text=response_text)
|
||||
|
||||
|
||||
@routes.get("/")
|
||||
async def index(request):
|
||||
"""Landingpage."""
|
||||
if not CRED_JSON.exists():
|
||||
raise web.HTTPFound("/auth")
|
||||
html_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
|
||||
return web.FileResponse(html_file)
|
||||
|
||||
|
||||
@routes.get("/auth")
|
||||
async def auth(request):
|
||||
"""Authenticate with google."""
|
||||
html_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "auth.html")
|
||||
with open(html_file) as _file:
|
||||
html = _file.read()
|
||||
html = html.replace("[[AUTH_URL]]", request.app["auth"].auth_url)
|
||||
return web.Response(text=html, content_type="text/html")
|
||||
|
||||
|
||||
app = web.Application()
|
||||
app.add_routes(routes)
|
||||
|
||||
with CLIENT_JSON.open("r") as data:
|
||||
user_data = json.load(data)["installed"]
|
||||
auth = AuthHandler(user_data, CRED_JSON)
|
||||
app.router.add_post("/token", auth.token)
|
||||
app["auth"] = auth
|
||||
|
||||
web.run_app(app, port=5000)
|
||||
Reference in New Issue
Block a user