diff --git a/modules/api.py b/modules/api.py index 75d5afb..bd7ab93 100644 --- a/modules/api.py +++ b/modules/api.py @@ -3,9 +3,12 @@ from flask import request, flash import flask_login -import json, os, sys, hashlib +import json +import os +import sys +import hashlib from werkzeug.security import generate_password_hash -from smarthome import report_state +from smarthome import rs from modules.database import db, User, Settings from modules.domoticz import getDomoticzDevices, queryDomoticz from modules.helpers import logger, remove_user @@ -14,7 +17,7 @@ def modifyServerSettings(request): dbsettings = Settings.query.get_or_404(1) - + dbsettings.client_id = request.args.get('aogclient', '') dbsettings.client_secret = request.args.get('aogsecret', '') dbsettings.api_key = request.args.get('aogapi', '') @@ -23,10 +26,10 @@ def modifyServerSettings(request): dbsettings.use_ssl = (request.args.get('ssl', '') == 'true') dbsettings.ssl_cert = request.args.get('sslcert', '') dbsettings.ssl_key = request.args.get('sslkey', '') - + db.session.add(dbsettings) db.session.commit() - + logger.info("Server settings saved") @@ -35,31 +38,30 @@ def modifyUserSettings(username, request): dbuser = User.query.filter_by(username=username).first() dbuser.domo_url = request.args.get('domourl', '') - dbuser.domouser = request.args.get('domouser','') - dbuser.domopass = request.args.get('domopass','') - dbuser.roomplan = request.args.get('roomplan','') - dbuser.password = request.args.get('uipassword','') + dbuser.domouser = request.args.get('domouser', '') + dbuser.domopass = request.args.get('domopass', '') + dbuser.roomplan = request.args.get('roomplan', '') + dbuser.password = request.args.get('uipassword', '') dbuser.googleassistant = (request.args.get('googleassistant', '') == 'true') - + db.session.add(dbuser) db.session.commit() - + logger.info("User settings updated") - + @flask_login.login_required def gateway(): - dbsettings = Settings.query.get_or_404(1) + dbuser = User.query.filter_by(username=flask_login.current_user.username).first() requestedUrl = request.url.split("/api") custom = request.args.get('custom', '') - newsettings = {} if custom == "sync": - if dbuser.googleassistant == True: - if report_state.enable_report_state(): + if dbuser.googleassistant is True: + if rs.report_state_enabled(): payload = {"agentUserId": flask_login.current_user.username} - report_state.call_homegraph_api('sync', payload) + rs.call_homegraph_api('sync', payload) result = '{"title": "RequestedSync", "status": "OK"}' flash("Devices synced with domoticz") else: @@ -69,12 +71,12 @@ def gateway(): getDomoticzDevices(flask_login.current_user.username) flash("Devices synced with domoticz") return "Devices synced with domoticz", 200 - + if custom == "restart": - + logger.info('Restarts smarthome server') os.execv(sys.executable, ['python'] + sys.argv) - + elif custom == "setArmLevel": armLevel = request.args.get('armLevel', '') seccode = request.args.get('seccode', '') @@ -83,16 +85,16 @@ def gateway(): elif custom == "server_settings": modifyServerSettings(request) - + elif custom == "user_settings": - + modifyUserSettings(flask_login.current_user.username, request) - + elif custom == "removeuser": userToRemove = request.args.get('user', '') - + removeuser = User.query.filter_by(username=userToRemove).first() - + db.session.delete(removeuser) db.session.commit() remove_user(userToRemove) @@ -100,10 +102,10 @@ def gateway(): return "User removed", 200 else: - + result = queryDomoticz(flask_login.current_user.username, requestedUrl[1]) try: return json.loads(result) - except: + except Exception: return "No results returned", 404 diff --git a/modules/domoticz.py b/modules/domoticz.py index eaa4d2d..d8dae53 100644 --- a/modules/domoticz.py +++ b/modules/domoticz.py @@ -34,7 +34,7 @@ def getDomain(device): devs = devs.replace(" ", "") devs = devs.replace("/", "") - devs = devs.replace("+", "") + devs = devs.replace("+", "") return devs @@ -46,7 +46,7 @@ def getDomain(device): def getDesc(user_id, device): user = User.query.filter_by(username=user_id).first() - + if device.id in user.device_config: desc = user.device_config[device.id] return desc @@ -99,11 +99,11 @@ def getAog(device, user_id=None): aog = AogState() aog.id = domain + "_" + device.get('idx') aog.name = { - 'name' : device.get('Name'), + 'name': device.get('Name'), 'nicknames': [] } if device.get('Type') in ['Light/Switch', 'Color Switch', 'Lighting 1', 'Lighting 2', 'Lighting 5', 'RFY', 'Value']: - aog.type = 'action.devices.types.LIGHT' + aog.type = 'action.devices.types.LIGHT' if device.get('Image') == 'WallSocket': aog.type = 'action.devices.types.OUTLET' if device.get('Image') in ['Generic', 'Phone']: @@ -112,9 +112,8 @@ def getAog(device, user_id=None): aog.type = 'action.devices.types.FAN' if device.get('Image') == 'Heating': aog.type = 'action.devices.types.HEATER' - if domain in ['Thermostat', 'Setpoint']: + if domain in ['Thermostat', 'Setpoint']: aog.type = 'action.devices.types.THERMOSTAT' - # Try to get device specific voice control configuration from Domoticz aog.customData['dzTags'] = False @@ -124,7 +123,7 @@ def getAog(device, user_id=None): if desc is not None: logger.debug(' tags found for idx %s in domoticz description.', aog.id) aog.customData['dzTags'] = True - + if desc is not None: n = desc.get('nicknames', None) if n is not None: @@ -140,7 +139,7 @@ def getAog(device, user_id=None): aog.willReportState = repState st = desc.get('devicetype', None) if st is not None and st.lower() in config.TYPES: - aog.type = 'action.devices.types.'+ st.upper() + aog.type = 'action.devices.types.' + st.upper() if domain in ['Thermostat', 'Setpoint']: minT = desc.get('minThreehold', None) if minT is not None: @@ -160,7 +159,7 @@ def getAog(device, user_id=None): aog.customData['camurl'] = camurl hide = desc.get('hide', False) if hide: - domain = domain +'_Hidden' + domain = domain + '_Hidden' aog.customData['idx'] = device.get('idx') aog.customData['domain'] = domain @@ -169,9 +168,9 @@ def getAog(device, user_id=None): if domain == 'Scene': aog.type = 'action.devices.types.SCENE' - aog.traits.append('action.devices.traits.Scene') + aog.traits.append('action.devices.traits.Scene') if domain == 'Security': - aog.type = 'action.devices.types.SECURITYSYSTEM' + aog.type = 'action.devices.types.SECURITYSYSTEM' aog.traits.append('action.devices.traits.ArmDisarm') aog.customData['protected'] = True aog.attributes = { @@ -180,17 +179,17 @@ def getAog(device, user_id=None): "level_name": "Arm Home", "level_values": [ {"level_synonym": ["armed home", "low security", "home and guarding", "level 1", "home", "SL1"], - "lang": "en"}, + "lang": "en"}, {"level_synonym": dbsettings.armlevels['armhome'], - "lang": dbsettings.language} + "lang": dbsettings.language} ] }, { "level_name": "Arm Away", "level_values": [ - {"level_synonym": ["armed away", "high security", "away and guarding", "level 2", "away", "SL2"], - "lang": "en"}, - {"level_synonym": dbsettings.armlevels['armaway'], - "lang": dbsettings.language} + {"level_synonym": ["armed away", "high security", "away and guarding", "level 2", "away", "SL2"], + "lang": "en"}, + {"level_synonym": dbsettings.armlevels['armaway'], + "lang": dbsettings.language} ] }], "ordered": True @@ -199,16 +198,16 @@ def getAog(device, user_id=None): if domain == 'Group': aog.type = 'action.devices.types.SWITCH' aog.traits.append('action.devices.traits.OnOff') - + if domain == 'SmokeDetector': - aog.type = 'action.devices.types.SMOKE_DETECTOR' - aog.traits.append('action.devices.traits.SensorState') - aog.attributes = {'sensorStatesSupported': [ - {'name': 'SmokeLevel', - 'descriptiveCapabilities': { - 'availableStates': [ - 'smoke detected', - 'no smoke detected'] + aog.type = 'action.devices.types.SMOKE_DETECTOR' + aog.traits.append('action.devices.traits.SensorState') + aog.attributes = {'sensorStatesSupported': [ + {'name': 'SmokeLevel', + 'descriptiveCapabilities': { + 'availableStates': [ + 'smoke detected', + 'no smoke detected'] } } ] @@ -216,11 +215,11 @@ def getAog(device, user_id=None): if domain in ['OnOff', 'Dimmer', 'PushOnButton', 'PushOffButton']: aog.traits.append('action.devices.traits.OnOff') if domain == 'Dimmer': - aog.traits.append('action.devices.traits.Brightness') + aog.traits.append('action.devices.traits.Brightness') if domain == 'DoorLock': aog.type = 'action.devices.types.LOCK' aog.traits.append('action.devices.traits.LockUnlock') - + if domain in ['VenetianBlindsUS', 'VenetianBlindsEU', 'Blinds', 'BlindsStop', 'BlindsPercentage']: aog.type = 'action.devices.types.BLINDS' aog.traits.append('action.devices.traits.OpenClose') @@ -228,16 +227,16 @@ def getAog(device, user_id=None): aog.traits.append('action.devices.traits.StartStop') if domain in ['VenetianBlindsUS', 'VenetianBlindsEU', 'Blinds']: aog.attributes = {'discreteOnlyOpenClose': True} - + if domain == 'ColorSwitch': aog.traits.append('action.devices.traits.OnOff') aog.traits.append('action.devices.traits.Brightness') aog.traits.append('action.devices.traits.ColorSetting') aog.attributes = {'colorModel': 'rgb', - 'colorTemperatureRange': { - 'temperatureMinK': 1700, - 'temperatureMaxK': 6500} - } + 'colorTemperatureRange': { + 'temperatureMinK': 1700, + 'temperatureMaxK': 6500} + } if domain == 'Selector': aog.type = 'action.devices.types.SWITCH' aog.traits.append('action.devices.traits.OnOff') @@ -248,7 +247,7 @@ def getAog(device, user_id=None): for s in levelName: levels.append( { - "name": s.replace(" ","_"), + "name": s.replace(" ", "_"), "name_values": [ {"name_synonym": [s], "lang": "en"}, @@ -269,20 +268,20 @@ def getAog(device, user_id=None): for s in levelName: levels.append( { - "name": s.replace(" ","_"), + "name": s.replace(" ", "_"), "name_values": [ {"name_synonym": [s], "lang": "en"}, {"name_synonym": [s], "lang": dbsettings.language}, ], - "settings":{ - "setting_name": s.replace(" ","_"), + "settings": { + "setting_name": s.replace(" ", "_"), "setting_values": [ {"setting_synonym": [s], - "lang": "en"}, + "lang": "en"}, {"setting_synonym": [s], - "lang": dbsettings.language}, + "lang": dbsettings.language}, ] } } @@ -290,24 +289,24 @@ def getAog(device, user_id=None): aog.attributes = {'availableModes': levels} # <-- Modes trait for selector not working yet - if domain in ['Temp', 'TempHumidity', 'TempHumidityBaro']: + if domain in ['Temp', 'TempHumidity', 'TempHumidityBaro']: aog.type = 'action.devices.types.SENSOR' aog.traits.append('action.devices.traits.TemperatureSetting') aog.attributes = {'thermostatTemperatureUnit': dbsettings.tempunit, - "thermostatTemperatureRange": { + 'thermostatTemperatureRange': { 'minThresholdCelsius': -30, 'maxThresholdCelsius': 40}, - 'queryOnlyTemperatureSetting': True, - 'availableThermostatModes': ['heat', 'cool'], + 'queryOnlyTemperatureSetting': True, + 'availableThermostatModes': ['heat', 'cool'], } - if domain in ['Thermostat', 'Setpoint']: + if domain in ['Thermostat', 'Setpoint']: aog.traits.append('action.devices.traits.TemperatureSetting') aog.attributes = {'thermostatTemperatureUnit': dbsettings.tempunit, - 'thermostatTemperatureRange': { - 'minThresholdCelsius': minThreehold, - 'maxThresholdCelsius': maxThreehold}, - 'availableThermostatModes': ['heat','cool'], - } + 'thermostatTemperatureRange': { + 'minThresholdCelsius': minThreehold, + 'maxThresholdCelsius': maxThreehold}, + 'availableThermostatModes': ['heat', 'cool'], + } if 'selector_modes_idx' in aog.customData: data = getDomoticzState(user_id, aog.customData['selector_modes_idx']) selectorModes = base64.b64decode(data['LevelNames']).decode('UTF-8').lower().split("|") @@ -343,28 +342,28 @@ def getAog(device, user_id=None): return aog -# Save device info in json format +# Save device info in json format def saveJson(user_id, data): - + datafile = user_id + "_devices.json" - filename = os.path.join(config.DEVICES_DIRECTORY, datafile) - + with open(filename, 'w') as fp: - json.dump(data, fp, default=lambda o: o.__dict__, - indent=4, ensure_ascii=False) - + json.dump(data, fp, default=lambda o: o.__dict__, + indent=4, ensure_ascii=False) + logger.info('Devices is saved in ' + datafile + ' in ' + config.DEVICES_DIRECTORY + ' folder') def queryDomoticz(username, url): + user = User.query.filter_by(username=username).first() domourl = user.domo_url domoCredits = (user.domouser, user.domopass) - + try: r = requests.get(domourl + '/json.htm' + url, - auth=domoCredits, timeout=5.00) + auth=domoCredits, timeout=5.00) except Exception: return "{}" @@ -374,7 +373,6 @@ def queryDomoticz(username, url): def getDomoticzDevices(user_id): user = User.query.filter_by(username=user_id).first() - aogDevs.clear() try: @@ -383,23 +381,22 @@ def getDomoticzDevices(user_id): logger.error("Error connection to domoticz!") saveJson(user_id, aogDevs) return - + devs = r['result'] - + for d in devs: aog = getAog(d, user_id) if aog is None: continue - + aogDevs[aog.id] = aog - + logger.info('Retreiving devices from domoticz') - saveJson(user_id, aogDevs) - + def getDomoticzState(user_id, idx, device='id'): - + if 'id' in device: url = '?type=command¶m=getdevices&rid=' + idx elif 'scene' in device: @@ -407,6 +404,6 @@ def getDomoticzState(user_id, idx, device='id'): r = json.loads(queryDomoticz(user_id, url)) devs = r['result'] for d in devs: - data = d - + data = d + return data diff --git a/modules/helpers.py b/modules/helpers.py index 6224468..ca25b06 100644 --- a/modules/helpers.py +++ b/modules/helpers.py @@ -3,7 +3,6 @@ import os import json import logging -import requests from flask import request, session, abort import random @@ -12,20 +11,21 @@ # Logging logging.basicConfig(level=logging.DEBUG, - format="%(asctime)s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - filename=os.path.join(config.CONFIG_DIRECTORY, "smarthome.log"), - filemode='w') + format="%(asctime)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + filename=os.path.join(config.CONFIG_DIRECTORY, "smarthome.log"), + filemode='w') logging.getLogger().addHandler(logging.StreamHandler()) logging.getLogger("urllib3").setLevel(logging.DEBUG) logging.getLogger('werkzeug').setLevel(logging.ERROR) logger = logging.getLogger() -def remove_user(user): + +def remove_user(user): devicesfile = os.path.join(config.DEVICES_DIRECTORY, user + "_devices.json") os.remove(devicesfile) - - + + # Function to retrieve token from header # def get_token(): auth = request.headers.get('Authorization') @@ -36,6 +36,7 @@ def get_token(): logger.warning("invalid token: %s", auth) return None + # Function to check current token, returns username # def check_token(): access_token = get_token() @@ -46,6 +47,7 @@ def check_token(): else: return None + # Function to load device info def get_device(user_id, device_id): filename = os.path.join(config.DEVICES_DIRECTORY, user_id + "_devices.json") @@ -58,7 +60,8 @@ def get_device(user_id, device_id): return data else: return None - + + # Function to load devices info def get_devices(user_id): filename = os.path.join(config.DEVICES_DIRECTORY, user_id + "_devices.json") @@ -70,11 +73,13 @@ def get_devices(user_id): else: return None + # Random string generator def random_string(stringLength=8): chars = string.ascii_letters + string.digits return ''.join(random.choice(chars) for i in range(stringLength)) - + + def _tempConvert(temp, unit): """ Convert Fahrenheit to Celsius """ if unit == 'F': @@ -82,23 +87,27 @@ def _tempConvert(temp, unit): return celsius else: return temp - + + def generateToken(last_code_user): access_token = random_string(32) logger.info("Access granted for " + last_code_user) return access_token - + + def csrfProtect(): if request.method == "POST": token = session.get('_csrf_token') if not token or token != request.form['_csrf_token']: abort(403) - + + def generateCsrfToken(): if '_csrf_token' not in session: session['_csrf_token'] = generate_API_key() return session['_csrf_token'] - + + def generate_API_key(): return random_string(16) diff --git a/modules/intents.py b/modules/intents.py new file mode 100644 index 0000000..1c3a19d --- /dev/null +++ b/modules/intents.py @@ -0,0 +1,121 @@ +import modules.trait as trait + +from itertools import product +from modules.reportstate import ReportState +from modules.helpers import ( + logger, + get_device, + get_devices, + random_string + ) + +repstate = ReportState() + + +class SmartHomeHandler: + + def statereport(self, requestId, userID, states): + """Send a state report to Google.""" + + data = {'requestId': requestId, + 'agentUserId': userID, + 'payload': { + 'devices': states + } + } + if 'notifications' in states: + data['eventId'] = random_string(10) + + repstate.call_homegraph_api('state', data) + + def sync(self, user_id): + + devices = [] + devs = get_devices(user_id) + + for device_id in devs.keys(): + device = get_device(user_id, device_id) + if 'Hidden' not in device['customData']['domain']: + device['deviceInfo'] = { + 'manufacturer': "Domoticz", + 'model': '2023.2', + 'hwVersion': '1.0', + 'swVersion': '1.0' + } + devices.append(device) + + return {"agentUserId": user_id, "devices": devices} + + def query(self, user_id, payload, requestId): + + devices = {} + devs = get_devices(user_id) + + for device in payload['devices']: + attr = devs.get(device['id']) + custom_data = device.get("customData", None) + devices[device['id']] = trait.query(custom_data, attr, user_id) + + if repstate.report_state_enabled(): + data = {'states': devices} + self.statereport(requestId, user_id, data) + + return {'devices': devices} + + def execute(self, user_id, commands, requestId): + + response = {'commands': []} + + for command in commands: + for device, execution in product(command['devices'], command['execution']): + + custom_data = device['customData'] + params = execution.get('params', None) + challenge = execution.get('challenge', None) + command = execution.get('command', None) + acknowledge = custom_data.get('acknowledge', None) + protected = custom_data.get('protected', None) + if protected: + acknowledge = False + if challenge is None: + action_result = { + "status": "ERROR", "errorCode": "challengeNeeded", "challengeNeeded": { + "type": "pinNeeded"}, "ids": [device['id']]} + response['commands'].append(action_result) + + return response + + elif not challenge.get('pin', False): + action_result = { + "status": "ERROR", "errorCode": "challengeNeeded", "challengeNeeded": { + "type": "userCancelled"}, "ids": [device['id']]} + response['commands'].append(action_result) + + return response + + if acknowledge: + if challenge is None: + action_result = { + "status": "ERROR", "errorCode": "challengeNeeded", "challengeNeeded": { + "type": "ackNeeded"}, "ids": [device['id']]} + response['commands'].append(action_result) + + return response + + action_result = trait.execute( + custom_data, command, params, user_id, challenge) + action_result['ids'] = [device['id']] + response['commands'].append(action_result) + + if repstate.report_state_enabled(): + data = {'states': {device['id']: action_result['states']}} + self.statereport(requestId, user_id, data) + + # if "followUpToken" in params and 'DoorLock' in custom_data['domain']: + # ndata = {'states':{},'notifications':{}} + # ndata['states'][device_id] = action_result['states'] + # ndata['notifications'][device_id] = {'LockUnlock':{"priority": 0,"followUpResponse": { + # "status": "SUCCESS", "followUpToken": params["followUpToken"], "isLocked":params['lock']}}} + # statereport(result['requestId'], user_id, data) + + return response diff --git a/modules/reportstate.py b/modules/reportstate.py index 1292ccd..c20a266 100644 --- a/modules/reportstate.py +++ b/modules/reportstate.py @@ -20,7 +20,7 @@ def __init__(self): self._access_token_expires = None @staticmethod - def enable_report_state(): + def report_state_enabled(): filename = os.path.join(config.KEYFILE_DIRECTORY, "smart-home-key.json") if os.path.isfile(filename): with open(filename, mode='r') as f: diff --git a/modules/routes.py b/modules/routes.py index 81abebf..d483ec1 100644 --- a/modules/routes.py +++ b/modules/routes.py @@ -1,6 +1,5 @@ import os import modules.config as config -import json from time import sleep from flask_login import login_required, current_user @@ -20,7 +19,7 @@ @login_required def dashboard(): - reportstate = report_state.enable_report_state() + reportstate = report_state.report_state_enabled() devices = get_devices(current_user.username) if devices is None: @@ -38,7 +37,7 @@ def dashboard(): @login_required def devices(): - reportstate = report_state.enable_report_state() + reportstate = report_state.report_state_enabled() dbsettings = Settings.query.get_or_404(1) devices = get_devices(current_user.username) dbuser = User.query.filter_by(username=current_user.username).first() @@ -63,8 +62,8 @@ def devices(): selector_modes_idx = request.form.get('selector_modes_idx') if idx not in deviceconfig.keys(): - deviceconfig[idx] = {} - + deviceconfig[idx] = {} + if hideDevice == 'on': deviceconfig[idx].update({'hide': True}) elif idx in deviceconfig.keys() and 'hide' in deviceconfig[idx]: @@ -79,7 +78,7 @@ def devices(): deviceconfig[idx].update({'ack': True}) elif idx in deviceconfig.keys() and 'ack' in deviceconfig[idx]: deviceconfig[idx].pop('ack') - + # if notification == 'on': # deviceconfig[idx].update({'notification': True}) # elif idx in deviceconfig.keys() and 'notification' in deviceconfig[idx]: @@ -90,12 +89,12 @@ def devices(): deviceconfig[idx].update({'room': room}) elif idx in deviceconfig.keys() and 'room' in deviceconfig[idx]: deviceconfig[idx].pop('room') - + if nicknames is not None: if nicknames != '': names = nicknames.split(", ") names = list(filter(None, names)) - deviceconfig[idx].update({'nicknames':names}) + deviceconfig[idx].update({'nicknames': names}) elif idx in deviceconfig.keys() and 'nicknames' in deviceconfig[idx]: deviceconfig[idx].pop('nicknames') @@ -140,7 +139,7 @@ def devices(): dbuser.device_config.update(deviceconfig) db.session.add(dbuser) - db.session.commit() + db.session.commit() armedhome = request.form.get('armedhome') armedaway = request.form.get('armedaway') @@ -282,7 +281,7 @@ def settings(): if request.method == "GET": dbsettings = Settings.query.get_or_404(1) dbusers = User.query.all() - reportstate = report_state.enable_report_state() + reportstate = report_state.report_state_enabled() devices = get_devices(current_user.username) return render_template('settings.html', diff --git a/smarthome.py b/smarthome.py index ba08cff..8ee89b3 100644 --- a/smarthome.py +++ b/smarthome.py @@ -1,36 +1,37 @@ # -*- coding: utf-8 -*- import flask_login -import importlib import urllib import os -import sys import json import modules.config as config import modules.api as api import modules.routes as routes from time import time -from itertools import product + from werkzeug.security import check_password_hash from werkzeug.middleware.proxy_fix import ProxyFix from modules.database import db, User, Settings from modules.domoticz import getDomoticzDevices from modules.reportstate import ReportState +from modules.intents import SmartHomeHandler from sqlalchemy import or_ -from modules.helpers import (logger, get_token, random_string, - get_device, get_devices, generateToken, - generateCsrfToken, csrfProtect) +from modules.helpers import ( + logger, + get_token, + random_string, + get_device, + generateToken, + generateCsrfToken, + csrfProtect, + ) from flask import (Flask, redirect, request, url_for, render_template, send_from_directory, jsonify, session) - -# Path to traits -sys.path.insert(0, 'modules') - app = Flask(__name__) # Create an actual secret key for production app.secret_key = 'secret' @@ -47,7 +48,8 @@ login_manager = flask_login.LoginManager() login_manager.init_app(app) -report_state = ReportState() +rs = ReportState() +smarthome = SmartHomeHandler() last_code = None last_code_user = None @@ -58,21 +60,6 @@ os.system('python3 init_db.py') -def statereport(requestId, userID, states): - """Send a state report to Google.""" - - data = {} - if 'notifications' in states: - data['eventId'] = random_string(10) - data['requestId'] = requestId - data['agentUserId'] = userID - data['payload'] = {} - data['payload']['devices'] = {} - data['payload']['devices'] = states - - report_state.call_homegraph_api('state', data) - - @login_manager.user_loader def user_loader(username): return db.session.get(User, int(username)) @@ -246,7 +233,7 @@ def notification(): return "Not found", 404 # Reportstate must be enabled to use notification - if report_state.enable_report_state(): + if rs.report_state_enabled(): request_id = random_string(20) try: @@ -260,18 +247,24 @@ def notification(): # Send smokedetektor notification if domain == 'SmokeDetector': - data['states'][message["id"]] = {"on": (True if message["state"].lower() in ['on', 'alarm/fire !'] else False)}, - data['notifications'][message["id"]] = {'SensorState': {'priority': 0, 'name': 'SmokeLevel', 'currentSensorState': 'smoke detected'}} + data['states'][message["id"]] = { + "on": (True if message["state"].lower() in ['on', 'alarm/fire !'] else False)}, + data['notifications'][message["id"]] = { + 'SensorState': { + 'priority': 0, 'name': 'SmokeLevel', 'currentSensorState': 'smoke detected'}} # Send smokedetektor notification elif domain == 'Doorbell': - data['states'][message["id"]] = {"on": (True if message["state"].lower() in ['on', 'pressed'] else False)}, - data['notifications'][message["id"]] = {"ObjectDetection": {"objects": {"unfamiliar": 1}, 'priority': 0, 'detectionTimestamp': time()}} + data['states'][message["id"]] = { + "on": (True if message["state"].lower() in ['on', 'pressed'] else False)}, + data['notifications'][message["id"]] = { + "ObjectDetection": { + "objects": {"unfamiliar": 1}, 'priority': 0, 'detectionTimestamp': time()}} else: return '{"title": "SendNotification", "status": "ERR"}' - statereport(request_id, user_id, data) + smarthome.statereport(request_id, user_id, data) return '{"title": "SendNotification", "status": "OK"}' return "Not found", 404 @@ -290,108 +283,32 @@ def fulfillment(): r = request.get_json() - logger.debug("request: \r\n%s", json.dumps(r, indent=4)) - - result = {} - result['requestId'] = r['requestId'] - result['payload'] = {} + logger.debug("request: \r\n%s", json.dumps(r, indent=2)) inputs = r['inputs'] + requestId = r['requestId'] + result = {'requestId': requestId, 'payload': {}} + for i in inputs: intent = i['intent'] """ Sync intent, need to response with devices list """ if intent == "action.devices.SYNC": - result['payload'] = {"agentUserId": user_id, "devices": []} + getDomoticzDevices(user_id) - devs = get_devices(user_id) - for device_id in devs.keys(): - # Loading device info - device = get_device(user_id, device_id) - if 'Hidden' not in device['customData']['domain']: - device['deviceInfo'] = { - "manufacturer": "Domoticz", - "model": "1", - "hwVersion": "1", - "swVersion": "1" - } - result['payload']['devices'].append(device) + sync = smarthome.sync(user_id) + result['payload'] = sync """ Query intent, need to response with current device status """ if intent == "action.devices.QUERY": - devs = get_devices(user_id) - result['payload']['devices'] = {} - for device in i['payload']['devices']: - device_id = device['id'] - x = devs.get(device_id) - custom_data = device.get("customData", None) - device_module = importlib.import_module('trait') - # Call query method for this device - query_method = getattr(device_module, "query") - result['payload']['devices'][device_id] = query_method(custom_data, x, user_id) - # ReportState - if report_state.enable_report_state(): - qdata = {} - qdata['states'] = result['payload']['devices'] - statereport(result['requestId'], user_id, qdata) + + query = smarthome.query(user_id, i['payload'], requestId) + result['payload'] = query """ Execute intent, need to execute some action """ if intent == "action.devices.EXECUTE": - result['payload'] = {} - result['payload']['commands'] = [] - for command in i['payload']['commands']: - for device, execution in product(command['devices'], command['execution']): - - entity_id = device['id'] - custom_data = device['customData'] - params = execution.get('params', None) - challenge = execution.get('challenge', None) - command = execution.get('command', None) - module = importlib.import_module('trait') - action = getattr(module, "execute") - acknowledge = custom_data.get('acknowledge', None) - protected = custom_data.get('protected', None) - if protected: - acknowledge = False - if challenge is None: - action_result = { - "status": "ERROR", "errorCode": "challengeNeeded", "challengeNeeded": { - "type": "pinNeeded"}, "ids": [entity_id]} - result['payload']['commands'].append(action_result) - logger.debug("response: \r\n%s", json.dumps(result, indent=4)) - return jsonify(result) - elif not challenge.get('pin', False): - action_result = { - "status": "ERROR", "errorCode": "challengeNeeded", "challengeNeeded": { - "type": "userCancelled"}, "ids": [entity_id]} - result['payload']['commands'].append(action_result) - logger.debug("response: \r\n%s", json.dumps(result, indent=4)) - return jsonify(result) - if acknowledge: - if challenge is None: - action_result = { - "status": "ERROR", "errorCode": "challengeNeeded", "challengeNeeded": { - "type": "ackNeeded"}, "ids": [entity_id]} - result['payload']['commands'].append(action_result) - logger.debug("response: \r\n%s", json.dumps(result, indent=4)) - return jsonify(result) - # Call execute method - action_result = action( - custom_data, command, params, user_id, challenge) - result['payload']['commands'].append(action_result) - action_result['ids'] = [entity_id] - - # ReportState - if report_state.enable_report_state() and action_result['status'] == 'SUCCESS': - data = {'states': {}} - data['states'][entity_id] = action_result['states'] - statereport(result['requestId'], user_id, data) - - # if "followUpToken" in params and 'DoorLock' in custom_data['domain']: - # ndata = {'states':{},'notifications':{}} - # ndata['states'][device_id] = action_result['states'] - # ndata['notifications'][device_id] = {'LockUnlock':{"priority": 0,"followUpResponse": { - # "status": "SUCCESS", "followUpToken": params["followUpToken"], "isLocked":params['lock']}}} - # statereport(result['requestId'], user_id, data) + + execute = smarthome.execute(user_id, i['payload']['commands'], requestId) + result['payload'] = execute """ Disconnect intent, need to revoke token """ if intent == "action.devices.DISCONNECT": @@ -402,7 +319,7 @@ def fulfillment(): return {} - logger.debug("response: \r\n%s", json.dumps(result, indent=4)) + logger.debug("response: \r\n%s", json.dumps(result, indent=2)) return jsonify(result) @@ -413,7 +330,7 @@ def fulfillment(): if __name__ == "__main__": logger.info("Smarthome server has started.") - if not report_state.enable_report_state(): + if not rs.report_state_enabled(): logger.info('Upload the smart-home-key.json to %s folder', config.KEYFILE_DIRECTORY) app.add_url_rule('/dashboard', 'dashboard', routes.dashboard, methods=['GET', 'POST']) app.add_url_rule('/devices', 'devices', routes.devices, methods=['GET', 'POST']) @@ -429,4 +346,4 @@ def fulfillment(): app.run('0.0.0.0', port=8181, debug=True, ssl_context=context) else: logger.info("Running without ssl") - app.run('0.0.0.0', port=8181, threaded=True, debug=True) + app.run('0.0.0.0', port=8181, debug=True)