Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ClusterM/google-assistant-smart-home.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2020-05-23 18:05:37 +0300
committerAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2020-05-23 18:05:37 +0300
commit9670691c4cb6d5dff706d4fad946835cf8209923 (patch)
treebb088bf5c1535e39be0fe6fcea878542de00f4cf
initial commit
-rw-r--r--config.py14
-rwxr-xr-xcss/style.css12
-rw-r--r--devices/pc.json25
-rw-r--r--devices/pc.py16
-rw-r--r--google_home.py202
-rw-r--r--google_home.wsgi4
-rwxr-xr-xsync.py19
-rwxr-xr-xtemplates/login.html20
-rw-r--r--users/demo_user.json6
9 files changed, 318 insertions, 0 deletions
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..63b8def
--- /dev/null
+++ b/config.py
@@ -0,0 +1,14 @@
+import logging
+
+CLIENT_ID = "YOUR_SMART_HOME_NAME"
+CLIENT_SECRET = "YOUR_SECRET"
+API_KEY = "YOUR_API_KEY"
+USERS_DIRECTORY = "/home/google_home/users"
+TOKENS_DIRECTORY = "/home/google_home/tokens"
+DEVICES_DIRECTORY = "/home/google_home/devices"
+
+# Uncomment to enable logging
+#LOG_FILE = "/var/log/google-home.log"
+#LOG_LEVEL = logging.DEBUG
+#LOG_FORMAT = "%(asctime)s %(remote_addr)s %(user)s %(message)s"
+#LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
diff --git a/css/style.css b/css/style.css
new file mode 100755
index 0000000..9285cfa
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,12 @@
+body {
+ background-color: #333333;
+ color: #FFFFFF;
+}
+
+input {
+ height: 1.5em; font-size: 1em;
+}
+
+.error {
+ color: red;
+}
diff --git a/devices/pc.json b/devices/pc.json
new file mode 100644
index 0000000..40282ed
--- /dev/null
+++ b/devices/pc.json
@@ -0,0 +1,25 @@
+{
+ "type": "action.devices.types.SWITCH",
+ "traits": [
+ "action.devices.traits.OnOff"
+ ],
+ "name": {
+ "name": "PC",
+ "defaultNames": [
+ "PC",
+ "Computer"
+ ],
+ "nicknames": [
+ "PC",
+ "Computer"
+ ]
+ },
+ "willReportState": false,
+ "roomHint": "My room",
+ "deviceInfo": {
+ "manufacturer": "Cluster",
+ "model": "1",
+ "hwVersion": "1",
+ "swVersion": "1"
+ }
+}
diff --git a/devices/pc.py b/devices/pc.py
new file mode 100644
index 0000000..e212789
--- /dev/null
+++ b/devices/pc.py
@@ -0,0 +1,16 @@
+import subprocess
+
+def pc_query(custom_data):
+ p = subprocess.run(["ping", "-c", "1", "192.168.0.2"], stdout=subprocess.PIPE)
+ state = p.returncode == 0
+ return {"on": state, "online": True}
+
+def pc_action(custom_data, command, params):
+ if command == "action.devices.commands.OnOff":
+ if params['on']:
+ subprocess.run(["wakeonlan", "-i", "192.168.0.255", "00:11:22:33:44:55"])
+ else:
+ subprocess.run(["sh", "-c", "echo shutdown -h | ssh clust@192.168.0.2"])
+ return {"status": "SUCCESS", "states": {"on": params['on'], "online": True}}
+ else:
+ return {"status": "ERROR"}
diff --git a/google_home.py b/google_home.py
new file mode 100644
index 0000000..5fcd811
--- /dev/null
+++ b/google_home.py
@@ -0,0 +1,202 @@
+# coding: utf8
+
+import config
+from flask import Flask
+from flask import request
+from flask import render_template
+from flask import send_from_directory
+from flask import redirect
+import sys
+import os
+import requests
+import urllib
+import json
+import random
+import string
+from time import time
+import importlib
+import logging
+
+if hasattr(config, 'LOG_FILE'):
+ logging.basicConfig(level=config.LOG_LEVEL,
+ format=config.LOG_FORMAT,
+ datefmt=config.LOG_DATE_FORMAT,
+ filename=config.LOG_FILE,
+ filemode='a')
+logger = logging.getLogger()
+
+sys.path.insert(0, config.DEVICES_DIRECTORY)
+
+last_code = None
+last_code_user = None
+last_code_time = None
+
+app = Flask(__name__)
+
+logger.info("Started.", extra={'remote_addr': '-', 'user': '-'})
+
+def get_user(username):
+ filename = os.path.join(config.USERS_DIRECTORY, username + ".json")
+ if os.path.isfile(filename) and os.access(filename, os.R_OK):
+ with open(filename, mode='r') as f:
+ text = f.read()
+ data = json.loads(text)
+ return data
+ else:
+ logger.warning("user not found", extra={'remote_addr': request.remote_addr, 'user': username})
+ return None
+
+def get_token():
+ auth = request.headers.get('Authorization')
+ parts = auth.split(' ', 2)
+ if len(parts) == 2 and parts[0].lower() == 'bearer':
+ return parts[1]
+ else:
+ logger.warning("invalid token: %s", auth, extra={'remote_addr': request.remote_addr, 'user': '-'})
+ return None
+
+def check_token():
+ access_token = get_token()
+ access_token_file = os.path.join(config.TOKENS_DIRECTORY, access_token)
+ if os.path.isfile(access_token_file) and os.access(access_token_file, os.R_OK):
+ with open(access_token_file, mode='r') as f:
+ return f.read()
+ else:
+ return None
+
+def get_device(device_id):
+ filename = os.path.join(config.DEVICES_DIRECTORY, device_id + ".json")
+ if os.path.isfile(filename) and os.access(filename, os.R_OK):
+ with open(filename, mode='r') as f:
+ text = f.read()
+ data = json.loads(text)
+ data['id'] = device_id
+ return data
+ else:
+ return None
+
+def random_string(stringLength=8):
+ chars = string.ascii_letters + string.digits
+ return ''.join(random.choice(chars) for i in range(stringLength))
+
+@app.route('/css/<path:path>')
+def send_css(path):
+ return send_from_directory('css', path)
+
+@app.route('/auth/', methods=['GET', 'POST'])
+def auth():
+ global last_code, last_code_user, last_code_time
+ if request.method == 'GET':
+ return render_template('login.html')
+ elif request.method == 'POST':
+ if ("username" not in request.form
+ or "password" not in request.form
+ or "state" not in request.args
+ or "response_type" not in request.args
+ or request.args["response_type"] != "code"
+ or "client_id" not in request.args
+ or request.args["client_id"] != config.CLIENT_ID):
+ logger.warning("invalid auth request", extra={'remote_addr': request.remote_addr, 'user': request.form['username']})
+ return "Invalid request", 400
+ user = get_user(request.form["username"])
+ if user == None or user["password"] != request.form["password"]:
+ logger.warning("invalid password", extra={'remote_addr': request.remote_addr, 'user': request.form['username']})
+ return render_template('login.html', login_failed=True)
+
+ last_code = random_string(8)
+ last_code_user = request.form["username"]
+ last_code_time = time()
+
+ params = {'state': request.args['state'],
+ 'code': last_code,
+ 'client_id': config.CLIENT_ID}
+ logger.info("generated code", extra={'remote_addr': request.remote_addr, 'user': request.form['username']})
+ return redirect(request.args["redirect_uri"] + '?' + urllib.parse.urlencode(params))
+
+@app.route('/token/', methods=['POST'])
+def token():
+ global last_code, last_code_user, last_code_time
+ if ("client_secret" not in request.form
+ or request.form["client_secret"] != config.CLIENT_SECRET
+ or "client_id" not in request.form
+ or request.form["client_id"] != config.CLIENT_ID
+ or "code" not in request.form):
+ logger.warning("invalid token request", extra={'remote_addr': request.remote_addr, 'user': last_code_user})
+ return "Invalid request", 400
+ if request.form["code"] != last_code:
+ logger.warning("invalid code", extra={'remote_addr': request.remote_addr, 'user': last_code_user})
+ return "Invalid code", 403
+ if time() - last_code_time > 10:
+ logger.warning("code is too old", extra={'remote_addr': request.remote_addr, 'user': last_code_user})
+ return "Code is too old", 403
+ access_token = random_string(32)
+ access_token_file = os.path.join(config.TOKENS_DIRECTORY, access_token)
+ with open(access_token_file, mode='wb') as f:
+ f.write(last_code_user.encode('utf-8'))
+ logger.info("access granted", extra={'remote_addr': request.remote_addr, 'user': last_code_user})
+ return {'access_token': access_token}
+
+@app.route('/', methods=['GET', 'POST'])
+def fulfillment():
+ if request.method == 'GET': return "Your smart home is ready."
+
+ user_id = check_token()
+ if user_id == None:
+ return "Access denied", 403
+ r = request.get_json()
+ logger.debug("request: \r\n%s", json.dumps(r, indent=4), extra={'remote_addr': request.remote_addr, 'user': user_id})
+
+ result = {}
+ result['requestId'] = r['requestId']
+
+ inputs = r['inputs']
+ for i in inputs:
+ intent = i['intent']
+ if intent == "action.devices.SYNC":
+ result['payload'] = {"agentUserId": user_id, "devices": []}
+ user = get_user(user_id)
+ for device_id in user['devices']:
+ device = get_device(device_id)
+ result['payload']['devices'].append(device)
+
+ if intent == "action.devices.QUERY":
+ result['payload'] = {}
+ result['payload']['devices'] = {}
+ for device in i['payload']['devices']:
+ device_id = device['id']
+ if "customData" in device:
+ custom_data = device['customData']
+ else:
+ custom_data = None
+ device_info = get_device(device_id)
+ device_module = importlib.import_module(device_id)
+ query_method = getattr(device_module, device_id + "_query")
+ result['payload']['devices'][device_id] = query_method(custom_data)
+
+ if intent == "action.devices.EXECUTE":
+ result['payload'] = {}
+ result['payload']['commands'] = []
+ for command in i['payload']['commands']:
+ for device in command['devices']:
+ device_id = device['id']
+ custom_data = device.get("customData", None)
+ device_info = get_device(device_id)
+ device_module = importlib.import_module(device_id)
+ action_method = getattr(device_module, device_id + "_action")
+ for e in command['execution']:
+ command = e['command']
+ params = e.get("params", None)
+ action_result = action_method(custom_data, command, params)
+ action_result['ids'] = [device_id]
+ result['payload']['commands'].append(action_result)
+
+ if intent == "action.devices.DISCONNECT":
+ access_token = get_token()
+ access_token_file = os.path.join(config.TOKENS_DIRECTORY, access_token)
+ if os.path.isfile(access_token_file) and os.access(access_token_file, os.R_OK):
+ os.remove(access_token_file)
+ logger.debug("token %s revoked", access_token, extra={'remote_addr': request.remote_addr, 'user': user_id})
+ return {}
+
+ logger.debug("response: \r\n%s", json.dumps(result, indent=4), extra={'remote_addr': request.remote_addr, 'user': user_id})
+ return result
diff --git a/google_home.wsgi b/google_home.wsgi
new file mode 100644
index 0000000..67a5f50
--- /dev/null
+++ b/google_home.wsgi
@@ -0,0 +1,4 @@
+import sys
+sys.path.insert(0, '/home/smarthome/google-home')
+
+from google_home import app as application
diff --git a/sync.py b/sync.py
new file mode 100755
index 0000000..597823e
--- /dev/null
+++ b/sync.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python3
+
+import requests
+import json
+import os
+import config
+
+users = os.listdir(config.USERS_DIRECTORY)
+for user_file in users:
+ user = user_file.replace(".json", "")
+ print("User:", user, "... ", end="", flush=True)
+ payload = {"agentUserId": user}
+ url = "https://homegraph.googleapis.com/v1/devices:requestSync?key=" + config.API_KEY
+ r = requests.post(url, data=json.dumps(payload))
+ if r.text.strip() == "{}":
+ print("OK")
+ else:
+ print("ERROR")
+ print(r.text)
diff --git a/templates/login.html b/templates/login.html
new file mode 100755
index 0000000..6494fbe
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta id="viewport" name="viewport" content="width=device-width, initial-scale=1" />
+<title>Умный дом Кластера</title>
+<link rel="stylesheet" href="/css/style.css">
+</head>
+<body>
+<form method="POST">
+{% if login_failed %}
+ <div class="error">Неправильное имя и/или пароль</div><br/>
+{% endif %}
+ Логин:<br/>
+ <input type="text" name="username" size="20" /><br/>
+ Пароль:<br/>
+ <input type="password" name="password" size="20" /><br/><br/>
+ <input type="submit" name="login" value="Войти">
+</form>
+</body>
+</html>
diff --git a/users/demo_user.json b/users/demo_user.json
new file mode 100644
index 0000000..27a7a3b
--- /dev/null
+++ b/users/demo_user.json
@@ -0,0 +1,6 @@
+{
+ "password": "test",
+ "devices": [
+ "pc"
+ ]
+}