add new
This commit is contained in:
23
google-assistant-webserver/Dockerfile
Executable file
23
google-assistant-webserver/Dockerfile
Executable file
@@ -0,0 +1,23 @@
|
||||
FROM python:3.8-slim
|
||||
|
||||
RUN set -x \
|
||||
# Install required system packages
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
jq \
|
||||
tzdata \
|
||||
portaudio19-dev \
|
||||
libffi-dev \
|
||||
libssl-dev
|
||||
|
||||
# install python packages
|
||||
COPY requirements.txt .
|
||||
RUN pip install --upgrade -r requirements.txt
|
||||
|
||||
EXPOSE 5000/tcp
|
||||
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY /app /usr/src/app/
|
||||
|
||||
CMD [ "python", "/usr/src/app/main.py" ]
|
||||
57
google-assistant-webserver/README.md
Normal file
57
google-assistant-webserver/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Marcelveldt's Hassio Add-ons: Google Assistant Webserver
|
||||
|
||||
## About
|
||||
|
||||
Webservice for the Google Assistant SDK
|
||||
Allow you to send (broadcast) commands to Google Assistant
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
The installation of this add-on is pretty straightforward and not different in
|
||||
comparison to installing any other Hass.io add-on.
|
||||
|
||||
1. [Add my Hass.io add-ons repository][repository] to your Hass.io instance.
|
||||
1. Install the "Google Assistant Webserver" add-on.
|
||||
1. Start the "Google Assistant Webserver" add-on.
|
||||
1. Check the logs of the add-on to see if everything went well.
|
||||
1. At the first start, you will need to authenticate with Google, use the "Open Web UI" button for that.
|
||||
1. Ready to go!
|
||||
|
||||
|
||||
## Usage in HomeAssistant
|
||||
|
||||
Once you've set-up the webserver, you can add the component to HomeAssistant as notify component (for the broadcasts) and as script for the custom actions.
|
||||
|
||||
### Broadcast component
|
||||
|
||||
```yaml
|
||||
notify:
|
||||
- name: Google Assistant
|
||||
platform: rest
|
||||
resource: http://YOUR_HASS_IP_HERE:5000/broadcast
|
||||
```
|
||||
|
||||
### Script component
|
||||
|
||||
```yaml
|
||||
|
||||
# define as rest_command in configuration
|
||||
rest_command:
|
||||
- google_assistant_command:
|
||||
url: 'http://YOUR_HASS_IP_HERE:5000/command?message={{ command }}'
|
||||
|
||||
|
||||
# example usage in script
|
||||
script:
|
||||
- google_cmd_test:
|
||||
service: rest_command.google_assistant_command
|
||||
data:
|
||||
command: "some command you want to throw at the assistant"
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[repository]: https://github.com/marcelveldt/hassio-addons-repo
|
||||
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)
|
||||
20
google-assistant-webserver/config.json
Executable file
20
google-assistant-webserver/config.json
Executable file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Google Assistant Webserver",
|
||||
"version": "0.0.10",
|
||||
"description": "Webservice for the Google Assistant SDK - Allow you to send (broadcast) commands to Google Assistant",
|
||||
"slug": "google_assistant_webserver",
|
||||
"startup": "application",
|
||||
"boot": "auto",
|
||||
"arch": ["armhf", "amd64", "aarch64"],
|
||||
"devices": ["/dev/snd:/dev/snd:rwm"],
|
||||
"ports": {
|
||||
"5000/tcp": 5000
|
||||
},
|
||||
"ingress": true,
|
||||
"ingress_port": 5000,
|
||||
"webui": "http://[HOST]:[PORT:5000]",
|
||||
"options": {
|
||||
},
|
||||
"schema": {
|
||||
}
|
||||
}
|
||||
BIN
google-assistant-webserver/logo.png
Executable file
BIN
google-assistant-webserver/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
4
google-assistant-webserver/requirements.txt
Normal file
4
google-assistant-webserver/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
aiohttp[speedups]
|
||||
google-assistant-sdk[samples]
|
||||
google-auth-oauthlib[tool]
|
||||
uvloop
|
||||
Reference in New Issue
Block a user