def report_state(self, states, token): """Send a state report to Google.""" data = { 'requestId': self._request_id, 'agentUserId': token.get('userAgentId', None), 'payload': { 'devices': { 'states': states, } } } ReportState.call_homegraph_api(REPORT_STATE_BASE_URL, data)
def forceDevicesSync(self): userAgent = self.getUserAgent() enableReport = ReportState.enable_report_state() if userAgent is None: return 500 # internal error data = {"agentUserId": userAgent} if enableReport: r = ReportState.call_homegraph_api(REQUEST_SYNC_BASE_URL, data) elif 'Homegraph_API_Key' in configuration and configuration['Homegraph_API_Key'] != 'ADD_YOUR HOMEGRAPH_API_KEY_HERE': r = ReportState.call_homegraph_api_key(REQUEST_SYNC_BASE_URL, data) else: logger.error("No configuration for request_sync available") return r
def smarthome_sync(self, payload, token): """Handle action.devices.SYNC request. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ devices = [] states = {} aogDevs.clear() getDevices() # sync all devices getSettings() enableReport = ReportState.enable_report_state() for state in aogDevs.values(): entity = _GoogleEntity(state) serialized = entity.sync_serialize() if serialized is None: continue devices.append(serialized) if state.report_state: try: states[entity.entity_id] = entity.query_serialize() except: continue if enableReport: t = threading.Thread(target=self.delay_report_state, args=(states, token)).start() return { 'agentUserId': token.get('userAgentId', None), 'devices': devices, }
def smarthome_query(self, payload, token): """Handle action.devices.QUERY request. https://developers.google.com/actions/smarthome/create-app#actiondevicesquery """ enableReport = ReportState.enable_report_state() response = {} devices = {} getDevices() for device in payload.get('devices', []): devid = device['id'] #_GoogleEntity(aogDevs.get(devid, None)).async_update() state = aogDevs.get(devid, None) if not state: # If we can't find a state, the device is offline devices[devid] = {'online': False} continue e = _GoogleEntity(state) devices[devid] = e.query_serialize() response = {'devices': devices} logger.info("Response " + json.dumps(response, indent=2, sort_keys=True, ensure_ascii=False)) if state.report_state == True and enableReport == True: self.report_state(devices, token) return {'devices': devices}
def sync_serialize(self): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ state = self.state enableReport = ReportState.enable_report_state() traits = self.traits() # Found no supported traits for this entity if not traits: return None if enableReport: reportState = state.report_state else: reportState = enableReport device = { 'id': state.entity_id, 'name': { 'name': state.name }, 'attributes': {}, 'traits': [trait.name for trait in traits], 'willReportState': reportState, 'deviceInfo': { 'manufacturer': "Domoticz", "model": state.hardware }, 'type': DOMOTICZ_TO_GOOGLE_TYPES[state.domain], } # use aliases aliases = state.nicknames if aliases: device['name']['nicknames'] = aliases # add room hint if annotated room = state.room if room: device['roomHint'] = room for trt in traits: device['attributes'].update(trt.sync_attributes()) return device
def settings(self, s): user = self.getSessionUser() if user is None or user.get('uid', '') == '': s.redirect('login?redirect_uri={0}'.format('settings')) return enableReport = ReportState.enable_report_state() update = checkupdate() public_url = getTunnelUrl() message = '' meta = '<!-- <meta http-equiv="refresh" content="5"> -->' code = readFile(os.path.join(FILE_DIR, CONFIGFILE)) templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION))
def smarthome_sync(self, payload, token): """Handle action.devices.SYNC request. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ devices = [] aogDevs.clear() getDevices() # sync all devices getSettings() enableReport = ReportState.enable_report_state() agent_user_id = token.get('userAgentId', None) for state in aogDevs.values(): entity = _GoogleEntity(state) serialized = entity.sync_serialize(agent_user_id) if serialized is None: continue devices.append(serialized) response = {'agentUserId': agent_user_id, 'devices': devices} return response
def smarthome_exec(self, payload, token): """Handle action.devices.EXECUTE request. https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute """ entities = {} results = {} states = {} enableReport = ReportState.enable_report_state() for command in payload['commands']: for device, execution in product(command['devices'], command['execution']): entity_id = device['id'] new_state = execution.get('params') # Happens if error occurred. Skip entity for further processing if entity_id in results: continue if entity_id not in entities: if len(aogDevs) == 0: getDevices() getSettings() state = aogDevs.get(entity_id, None) if state is None: results[entity_id] = { 'ids': [entity_id], 'status': 'ERROR', 'errorCode': ERR_DEVICE_OFFLINE } continue entities[entity_id] = _GoogleEntity(state) try: entities[entity_id].execute( execution['command'], execution.get('params', {}), execution.get('challenge', None)) except SmartHomeError as err: results[entity_id] = { 'ids': [entity_id], 'status': 'ERROR', 'errorCode': err.code } logger.error(err) except SmartHomeErrorNoChallenge as err: results[entity_id] = { 'ids': [entity_id], 'status': 'ERROR', 'errorCode': err.code, 'challengeNeeded': { 'type': err.desc } } logger.error(err) final_results = list(results.values()) for entity in entities.values(): if entity.entity_id in results: continue entity.async_update() # final_results.append({'ids': [entity.entity_id], 'status': 'SUCCESS', 'states': entity.query_serialize()}) final_results.append({ 'ids': [entity.entity_id], 'status': 'SUCCESS', 'states': new_state }) if state.report_state: try: # states[entity.entity_id] = entity.query_serialize() states[entity.entity_id] = new_state except: continue if state.report_state == True and enableReport == True: self.report_state(states, token) return {'commands': final_results}
def settings_post(self, s): enableReport = ReportState.enable_report_state() update = checkupdate() confJSON = json.dumps(configuration) public_url = getTunnelUrl() logs = readFile(os.path.join(logfilepath, LOGFILE)) code = readFile(os.path.join(FILE_DIR, CONFIGFILE)) meta = '<!-- <meta http-equiv="refresh" content="5"> -->' if s.form.get("save"): textToSave = s.form.get("save", None) codeToSave = textToSave.replace("+", " ") saveFile(CONFIGFILE, codeToSave) message = 'Config saved' logger.info(message) logs = readFile(os.path.join(logfilepath, LOGFILE)) code = readFile(os.path.join(FILE_DIR, CONFIGFILE)) template = TEMPLATE.format(message=message, uptime=uptime(), list=deviceList, meta=meta, code=code, conf=confJSON, public_url=public_url, logs=logs, update=update) s.send_message(200, template) if s.form.get("backup"): codeToSave = readFile(os.path.join(FILE_DIR, CONFIGFILE)) saveFile('config/config.yaml.bak', codeToSave) message = 'Backup saved' logger.info(message) logs = readFile(os.path.join(logfilepath, LOGFILE)) template = TEMPLATE.format(message=message, uptime=uptime(), list=deviceList, meta=meta, code=code, conf=confJSON, public_url=public_url, logs=logs, update=update) s.send_message(200, template) if s.form.get("restart"): message = 'Restart Server, please wait a minute!' meta = '<meta http-equiv="refresh" content="20">' code = '' logs = '' template = TEMPLATE.format(message=message, uptime=uptime(), list=deviceList, meta=meta, code=code, conf=confJSON, public_url=public_url, logs=logs, update=update) s.send_message(200, template) restartServer() if s.form.get("sync"): if 'Homegraph_API_Key' in configuration and configuration[ 'Homegraph_API_Key'] != 'ADD_YOUR HOMEGRAPH_API_KEY_HERE' or enableReport == True: r = self.forceDevicesSync() time.sleep(0.5) if r: message = 'Devices syncronized' else: message = 'Homegraph api key not valid!' else: message = 'Add Homegraph api key or a Homegraph Service Account json file to sync devices here!' logs = readFile(os.path.join(logfilepath, LOGFILE)) template = TEMPLATE.format(message=message, uptime=uptime(), list=deviceList, meta=meta, code=code, conf=confJSON, public_url=public_url, logs=logs, update=update) s.send_message(200, template) if s.form.get("reload"): message = '' template = TEMPLATE.format(message=message, uptime=uptime(), list=deviceList, meta=meta, code=code, conf=confJSON, public_url=public_url, logs=logs, update=update) s.send_message(200, template) if s.form.get("deletelogs"): logfile = os.path.join(logfilepath, LOGFILE) if os.path.exists(logfile): f = open(logfile, 'w') f.close() logger.info('Logs removed by user') message = 'Logs removed' logs = readFile(os.path.join(logfilepath, LOGFILE)) template = TEMPLATE.format(message=message, uptime=uptime(), list=deviceList, meta=meta, code=code, conf=confJSON, public_url=public_url, logs=logs, update=update) s.send_message(200, template) if s.form.get("update"): repo.git.reset('--hard') repo.remotes.origin.pull() message = 'Updating to latest ' + repo.active_branch.name + ', please wait a minute!' meta = '<meta http-equiv="refresh" content="20">' template = TEMPLATE.format(message=message, uptime=uptime(), list=deviceList, meta=meta, code=code, conf=confJSON, public_url=public_url, logs=logs, update=update) s.send_message(200, template) subprocess.call([ 'pip', 'install', '-r', os.path.join(FILE_DIR, 'requirements/pip-requirements.txt') ]) restartServer()
def getAog(device): domain = AogGetDomain(device) if domain is None: return None aog = AogState() aog.name = device["Name"] # .encode('ascii', 'ignore') aog.domain = domain aog.id = device["idx"] aog.entity_id = domain + aog.id aog.state = device.get("Data", "Scene") aog.level = device.get("LevelInt", 0) aog.temp = device.get("Temp") aog.humidity = device.get("Humidity") aog.setpoint = device.get("SetPoint") aog.color = device.get("Color") aog.protected = device.get("Protected") aog.maxdimlevel = device.get("MaxDimLevel") aog.seccode = settings.get("SecPassword") aog.secondelay = settings.get("SecOnDelay") aog.tempunit = settings.get("TempUnit") aog.battery = device.get("BatteryLevel") aog.hardware = device.get("HardwareName") aog.selectorLevelName = device.get("LevelNames") aog.language = settings.get("Language") if lightDOMAIN == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS if outletDOMAIN == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS if colorDOMAIN == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS if colorDOMAIN == aog.domain and "RGBWW" == device["SubType"]: aog.attributes = ATTRS_COLOR_TEMP if climateDOMAIN == aog.domain and "Thermostat" == device["Type"]: aog.attributes = ATTRS_THERMSTATSETPOINT if blindsDOMAIN == aog.domain and "Blinds Percentage" == device[ "SwitchType"]: aog.attributes = ATTRS_PERCENTAGE if blindsDOMAIN == aog.domain and "Blinds Percentage Inverted" == device[ "SwitchType"]: aog.attributes = ATTRS_PERCENTAGE # Try to get device specific voice control configuration from Domoticz # Read it from the configuration file if not in Domoticz (for backward compatibility) desc = getDeviceConfig(device.get("Description")) if desc is None: desc = getDesc(aog) if desc is not None: n = desc.get('nicknames', None) if n is not None: aog.nicknames = n r = desc.get('room', None) if r is not None: aog.room = r ack = desc.get('ack', False) if ack: aog.ack = ack report_state = desc.get('report_state', True) if not ReportState.enable_report_state(): aog.report_state = False if not report_state: aog.report_state = report_state if aog.domain == cameraDOMAIN: aog.report_state = False return aog
DOMOTICZ_URL + '/json.htm?type=command¶m=addlogmessage&message=Connected to Google Assistant with DZGA v' + VERSION, auth=(configuration['Domoticz']['username'], configuration['Domoticz']['password']), timeout=(2, 5)) except Exception as e: logger.error('Connection to Domoticz refused with error: %s' % e) try: import git repo = git.Repo(FILE_DIR) except: repo = None ReportState = ReportState() if not ReportState.enable_report_state(): logger.error("Service account key is not found.") logger.error("Report state will be unavailable") def checkupdate(): if 'CheckForUpdates' in configuration and configuration[ 'CheckForUpdates'] == True: try: r = requests.get( 'https://raw.githubusercontent.com/DewGew/Domoticz-Google-Assistant/' + repo.active_branch.name + '/const.py') text = r.text if VERSION not in text: update = 1
def settings_post(self, s): enableReport = ReportState.enable_report_state() update = checkupdate() public_url = getTunnelUrl() code = readFile(os.path.join(FILE_DIR, CONFIGFILE)) meta = '<!-- <meta http-equiv="refresh" content="5"> -->' if s.form.get("save"): textToSave = s.form.get("save", None) codeToSave = textToSave.replace("+", " ") saveFile(CONFIGFILE, codeToSave) message = 'Configuration saved. Restart DZGA for the settings to take effect' logger.info(message) logs = readFile(os.path.join(logfilepath, LOGFILE)) templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION)) if s.form.get("backup"): codeToSave = readFile(os.path.join(FILE_DIR, CONFIGFILE)) saveFile('config/config.yaml.bak', codeToSave) message = 'Backup saved' logger.info(message) templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION)) if s.form.get("restart"): meta = '<meta http-equiv="refresh" content="10">' message = 'Restarts DZGA server' templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION)) restartServer() if s.form.get("sync"): if 'Homegraph_API_Key' in configuration and configuration['Homegraph_API_Key'] != 'ADD_YOUR HOMEGRAPH_API_KEY_HERE' or enableReport == True: r = self.forceDevicesSync() time.sleep(0.5) if r: message = 'Devices syncronized' else: message = 'Homegraph api key not valid!' else: message = 'Add Homegraph api key or a Homegraph Service Account json file to sync devices in the UI! You can still sync by voice eg. "Hey Google, Sync my devices".' templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION)) if s.form.get("reload"): message = '' templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION)) if s.form.get("deletelogs"): logfile = os.path.join(logfilepath, LOGFILE) if os.path.exists(logfile): f = open(logfile, 'w') f.close() logger.info('Logs removed by user') message = 'Logs removed' templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION)) if s.form.get("update"): repo.git.reset('--hard') repo.remotes.origin.pull() message = 'Updating to latest ' + branch + ', please wait a minute!' meta = '<meta http-equiv="refresh" content="20">' templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION)) subprocess.call(['pip3', 'install','-r', os.path.join(FILE_DIR, 'requirements/pip-requirements.txt')]) restartServer() if s.form.get("saveSettings"): savedSettings = json.loads(s.form.get("saveSettings", None)) with open(os.path.join(FILE_DIR, CONFIGFILE), 'r') as conf_file: newsettings = yaml.safe_load(conf_file) newsettings.update(savedSettings) saveFile(CONFIGFILE, yaml.safe_dump(newsettings, allow_unicode=True)) logger.info(yaml.dump(savedSettings)) message = 'Settings saved. Restart DZGA for the settings to take effect' logger.info(message) logs = readFile(os.path.join(logfilepath, LOGFILE)) templatepage = env.get_template('home.html') s.send_message(200, templatepage.render(message=message, uptime=uptime(), meta=meta, code=code, conf=configuration, public_url=public_url, update=update, keyfile=enableReport, branch=branch, dzversion=settings['dzversion'], dzgaversion=VERSION))
def getAog(device): domain = AogGetDomain(device) if domain is None: return None aog = AogState() aog.name = device["Name"] # .encode('ascii', 'ignore') aog.domain = domain aog.id = device["idx"] aog.entity_id = domain + aog.id aog.plan = device.get("PlanID") aog.state = device.get("Data", "Scene") aog.level = device.get("LevelInt", 0) aog.temp = device.get("Temp") aog.humidity = device.get("Humidity") aog.setpoint = device.get("SetPoint") if aog.domain is "Color": aog.color = device.get("Color") aog.protected = device.get("Protected") aog.maxdimlevel = device.get("MaxDimLevel") aog.battery = device.get("BatteryLevel") aog.hardware = device.get("HardwareName") aog.selectorLevelName = device.get("LevelNames") aog.lastupdate = device.get("LastUpdate") aog.language = settings.get("Language") aog.tempunit = settings.get("TempUnit") if aog.domain is "Security": aog.seccode = settings.get("SecPassword") aog.secondelay = settings.get("SecOnDelay") # Try to get device specific voice control configuration from Domoticz # Read it from the configuration file if not in Domoticz (for backward compatibility) desc = getDeviceConfig(device.get("Description")) if desc is not None: logger.debug('<voicecontrol> tags found for idx %s in domoticz description.', aog.id) logger.debug('Device_Config for idx %s will be ignored in config.yaml!', aog.id) if desc is None: desc = getDesc(aog) if desc is not None: dt = desc.get('devicetype', None) if dt is not None: if aog.domain in [domains['blinds']]: if dt.lower() in ['window', 'gate', 'garage', 'door']: aog.domain = domains[dt.lower()] if aog.domain in [domains['light'], domains['switch']]: if dt.lower() in ['window', 'door', 'gate', 'garage', 'light', 'ac_unit', 'bathtub', 'coffeemaker', 'dishwasher', 'dryer', 'fan', 'heater', 'kettle', 'media', 'microwave', 'outlet', 'oven', 'speaker', 'switch', 'vacuum', 'washer', 'waterheater']: aog.domain = domains[dt.lower()] if aog.domain in [domains['door']]: if dt.lower() in ['window', 'gate', 'garage']: aog.domain = domains[dt.lower()] if aog.domain in [domains['selector']]: if dt.lower() in ['vacuum']: aog.domain = domains[dt.lower()] pn = desc.get('name', None) if pn is not None: aog.name = pn n = desc.get('nicknames', None) if n is not None: aog.nicknames = n r = desc.get('room', None) if r is not None: aog.room = r ack = desc.get('ack', False) if ack: aog.ack = ack report_state = desc.get('report_state', True) if not ReportState.enable_report_state(): aog.report_state = False if not report_state: aog.report_state = report_state if domains['thermostat'] == aog.domain: at_idx = desc.get('actual_temp_idx', None) if at_idx is not None: aog.actual_temp_idx = at_idx try: aog.state = str(aogDevs[domains['temperature'] + at_idx].temp) aogDevs[domains['temperature'] + at_idx].domain = domains['merged'] + aog.id + ')' except: logger.error('Merge Error, Cant find temperature device with idx %s', at_idx) modes_idx = desc.get('selector_modes_idx', None) if modes_idx is not None: aog.modes_idx = modes_idx try: aog.level = aogDevs[domains['selector'] + modes_idx].level aog.selectorLevelName = aogDevs[domains['selector'] + modes_idx].selectorLevelName aogDevs[domains['selector'] + modes_idx].domain = domains['merged'] + aog.id + ')' except: logger.error('Merge Error, Cant find selector device with idx %s', modes_idx) if aog.domain in [domains['heater'], domains['kettle'], domains['waterheater'], domains['oven']]: tc_idx = desc.get('merge_thermo_idx', None) if tc_idx is not None: aog.merge_thermo_idx = tc_idx try: aog.temp = aogDevs[domains['thermostat'] + tc_idx].state aog.setpoint = aogDevs[domains['thermostat'] + tc_idx].setpoint aogDevs[domains['thermostat'] + tc_idx].domain = domains['merged'] + aog.id + ')' except: logger.error('Merge Error, Cant find thermostat device with idx %s', tc_idx) hide = desc.get('hide', False) if hide: if aog.domain not in [domains['scene'], domains['group']]: aog.domain = domains['hidden'] else: logger.error('Scenes and Groups does not support function "hide" yet') if aog.domain in [domains['camera']]: aog.report_state = False if domains['light'] == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS if domains['fan'] == aog.domain and "Selector" == device["SwitchType"]: aog.attributes = ATTRS_FANSPEED if domains['outlet'] == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS if domains['color'] == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS if domains['color'] == aog.domain and device["SubType"] in ["RGBWW", "White"]: aog.attributes = ATTRS_COLOR_TEMP if domains['thermostat'] == aog.domain and "Thermostat" == device["Type"]: aog.attributes = ATTRS_THERMSTATSETPOINT if domains['blinds'] == aog.domain and "Blinds Percentage" == device["SwitchType"]: aog.attributes = ATTRS_PERCENTAGE if domains['blindsinv'] == aog.domain and "Blinds Percentage Inverted" == device["SwitchType"]: aog.attributes = ATTRS_PERCENTAGE if domains['vacuum'] == aog.domain and "Selector" == device["SwitchType"]: aog.attributes = ATTRS_VACUUM_MODES if aog.room == None: if aog.domain not in [domains['scene'], domains['group']]: if aog.plan is not "0": aog.room = getPlans(aog.plan) return aog
auth=(configuration['Domoticz']['username'], configuration['Domoticz']['password'])) except Exception as e: logger.error('Connection to Domoticz refused with error: %s' % (e)) try: import git except ImportError: logger.info('Installing package GitPython') pip.main(['install', 'gitpython']) import git try: repo = git.Repo(FILE_DIR) except: repo = None ReportState = ReportState() if ReportState.enable_report_state() == False: logger.error("Service account key is not found.") logger.error("Report state will be unavailable") def checkupdate(): if 'CheckForUpdates' in configuration and configuration[ 'CheckForUpdates'] == True: try: r = requests.get( 'https://raw.githubusercontent.com/DewGew/Domoticz-Google-Assistant/' + repo.active_branch.name + '/const.py') text = r.text if VERSION not in text: update = 1