diff options
author | Alexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com> | 2020-05-23 18:05:37 +0300 |
---|---|---|
committer | Alexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com> | 2020-05-23 18:05:37 +0300 |
commit | 9670691c4cb6d5dff706d4fad946835cf8209923 (patch) | |
tree | bb088bf5c1535e39be0fe6fcea878542de00f4cf |
initial commit
-rw-r--r-- | config.py | 14 | ||||
-rwxr-xr-x | css/style.css | 12 | ||||
-rw-r--r-- | devices/pc.json | 25 | ||||
-rw-r--r-- | devices/pc.py | 16 | ||||
-rw-r--r-- | google_home.py | 202 | ||||
-rw-r--r-- | google_home.wsgi | 4 | ||||
-rwxr-xr-x | sync.py | 19 | ||||
-rwxr-xr-x | templates/login.html | 20 | ||||
-rw-r--r-- | users/demo_user.json | 6 |
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 @@ -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" + ] +} |