diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 9c642da9d..b980817df 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,11 +1,10 @@ # This workflow will install Python dependencies and run tests with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Pytest and Sonarcloud analysis +name: Unittest & Quality on: push: - pull_request: jobs: test: @@ -22,15 +21,15 @@ jobs: with: python-version: 3.7 - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements_test.txt + uses: py-actions/py-dependency-install@v2 + with: + path: requirements_test.txt + - name: Install Portaudio + run: sudo apt-get install libportaudio2 - name: Tests - run: | - pytest tests/ --cov=core/ --cov-report=xml + run: pytest tests/ --cov=core/ --cov-report=xml - name: Fix paths - run: | - sed -i 's/\/home\/runner\/work\/ProjectAlice\/ProjectAlice\//\/github\/workspace\//g' coverage.xml + run: sed -i 's/\/home\/runner\/work\/ProjectAlice\/ProjectAlice\//\/github\/workspace\//g' coverage.xml - name: Sonarcloud scan uses: sonarsource/sonarcloud-github-action@master env: diff --git a/.gitprefix b/.gitprefix new file mode 100644 index 000000000..756dfefb8 --- /dev/null +++ b/.gitprefix @@ -0,0 +1,28 @@ +:art: cleanup +:rocket: deploy +:pencil2: typo +:construction: WIP +:heavy_plus_sign: add dependency +:heavy_minus_sign: remove dependency +:speaker: add logs +:mute: remove logs +:bug: fix +:globe_with_meridians: i18n +:poop: crap +:boom: breaking +:beers: drunk coding +:lipstick: cosmetic +:lock: fix security issue +:heavy_exclamation_mark: woot +:white_check_mark: add test +:green_heart: fix ci +:recycle: refactor +:children_crossing: improve user experience +:wastebasket: deprecated +:see_no_evil: gitignore +:alien: api change +:sparkles: new feature +:wheelchair: improve accessibility +:zap: improve performance +:fire: remove code or file +:ambulance: hotfix diff --git a/README.md b/README.md index 3416adfee..b8468e642 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@

License Discord
+ Tests + ZenHub logo
Coverage Status Maintainability Code Smells diff --git a/config-schema.json b/config-schema.json index 8678c05d6..fe8be1335 100644 --- a/config-schema.json +++ b/config-schema.json @@ -44,7 +44,9 @@ "mqtt", "tts", "wakeword", - "advanced debug" + "advanced debug", + "audio", + "scenarios" ] }, "isSensitive" : { @@ -66,11 +68,19 @@ }, "beforeUpdate": { "type" : "string", - "pattern": "^[a-z].*$" + "pattern": "^(?:[A-Z][a-zA-Z]+\\.)?[a-z][a-zA-Z0-9]+$" }, "onUpdate" : { "type" : "string", - "pattern": "^[a-z].*$" + "pattern": "^(?:[A-Z][a-zA-Z]+\\.)?[a-z][a-zA-Z0-9]+$" + }, + "onStart" : { + "type" : "string", + "pattern": "^(?:[A-Z][a-zA-Z]+\\.)?[a-z][a-zA-Z0-9]+$" + }, + "onInit" : { + "type" : "string", + "pattern": "^(?:[A-Z][a-zA-Z]+\\.)?[a-z][a-zA-Z0-9]+$" }, "parent" : { "type" : "object", diff --git a/configTemplate.json b/configTemplate.json index 30a698fe4..476215036 100644 --- a/configTemplate.json +++ b/configTemplate.json @@ -35,14 +35,49 @@ "isSensitive" : false, "description" : "Your asound settings", "beforeUpdate": "injectAsound", - "category" : "system" + "category" : "audio", + "parent" : { + "config" : "disableSoundAndMic", + "condition": "isnot", + "value" : true + } }, "recordAudioAfterWakeword": { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, "description" : "Allow audio record after a wakeword is detected to keep the last user speech. Can be usefull for recording skills", - "category" : "system" + "category" : "audio" + }, + "outputDevice" : { + "defaultValue": "", + "dataType" : "list", + "isSensitive" : false, + "values" : [], + "description" : "The device to use to play sounds", + "category" : "audio", + "onInit" : "populateAudioOutputConfig", + "onUpdate" : "AudioServer.updateAudioDevices", + "parent" : { + "config" : "disableSoundAndMic", + "condition": "isnot", + "value" : true + } + }, + "inputDevice" : { + "defaultValue": "", + "dataType" : "list", + "isSensitive" : false, + "values" : [], + "description" : "The device to use to record sounds", + "category" : "audio", + "onInit" : "populateAudioInputConfig", + "onUpdate" : "AudioServer.updateAudioDevices", + "parent" : { + "config" : "disableSoundAndMic", + "condition": "isnot", + "value" : true + } }, "deviceName" : { "defaultValue": "default", @@ -72,45 +107,60 @@ "dataType" : "string", "isSensitive" : true, "description" : "API key for IBM Cloud Watson Tts and ASR", - "category" : "credentials" + "category" : "credentials", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "ibmCloudAPIURL" : { "defaultValue": "", "dataType" : "string", "isSensitive" : false, "description" : "API url for IBM Cloud Watson Tts and ASR", - "category" : "credentials" + "category" : "credentials", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "autoReportSkillErrors" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, "description" : "If true, an error thrown by a skill will automatically post a github issue and ping the author", - "category" : "system" + "category" : "system", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, - "disableSoundAndMic" : { + "disableSoundAndMic" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, "description" : "If this device is a server without sound and mic, turn this to true", "onUpdate" : "enableDisableSound", - "category" : "device" + "category" : "audio" }, - "notUnderstoodRetries": { + "notUnderstoodRetries" : { "defaultValue": 3, "dataType" : "integer", "isSensitive" : false, "description" : "Defines how many times Alice will ask to repeat if not understood before she gives up", "category" : "system" }, - "ssid" : { + "ssid" : { "defaultValue": "", "dataType" : "string", "isSensitive" : false, "description" : "Your Wifi name", "category" : "device" }, - "debug" : { + "debug" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, @@ -118,14 +168,14 @@ "onUpdate" : "toggleDebugLogs", "category" : "system" }, - "advancedDebug" : { + "advancedDebug" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, "description" : "If true advanced debugging will be enabled. This activates extra information and tools for debugging.", "category" : "advanced debug" }, - "memoryProfiling" : { + "memoryProfiling" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, @@ -137,7 +187,7 @@ "value" : true } }, - "databaseProfiling" : { + "databaseProfiling" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, @@ -149,14 +199,14 @@ "value" : true } }, - "wifipassword" : { + "wifipassword" : { "defaultValue": "", "dataType" : "string", "isSensitive" : true, "description" : "Your Wifi password", "category" : "device" }, - "mqttHost" : { + "mqttHost" : { "defaultValue": "localhost", "dataType" : "string", "isSensitive" : false, @@ -164,7 +214,7 @@ "onUpdate" : "updateMqttSettings", "category" : "mqtt" }, - "mqttPort" : { + "mqttPort" : { "defaultValue": 1883, "dataType" : "integer", "isSensitive" : false, @@ -408,14 +458,24 @@ "dataType" : "string", "isSensitive" : true, "description" : "Your Amazon services access key", - "category" : "credentials" + "category" : "credentials", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "awsSecretKey" : { "defaultValue": "", "dataType" : "string", "isSensitive" : true, "description" : "Your Amazon services secret key", - "category" : "credentials" + "category" : "credentials", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "useHLC" : { "defaultValue": false, @@ -433,7 +493,8 @@ "fr", "de", "it", - "pt" + "pt", + "pl" ], "description" : "Project Alice active language", "category" : "system" @@ -450,42 +511,72 @@ "dataType" : "string", "isSensitive" : false, "description" : "If you want to use a non natively supported language, set it here.", - "category" : "system" + "category" : "system", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "nonNativeSupportCountry" : { "defaultValue": "", "dataType" : "string", "isSensitive" : false, "description" : "If you want to use a non natively supported country, set it here.", - "category" : "system" + "category" : "system", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "aliceAutoUpdate" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, "description" : "Whether Alice should auto update, checked every hour", - "category" : "system" + "category" : "system", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "skillAutoUpdate" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, "description" : "Whether skills should auto update, checked every 15 minutes", - "category" : "system" + "category" : "system", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "githubUsername" : { "defaultValue": "", "dataType" : "string", "isSensitive" : false, "description" : "Not mendatory, your github username and token allows you to use Github API much more, such as checking for skills, updating them etc etc", - "category" : "credentials" + "category" : "credentials", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "githubToken" : { "defaultValue": "", "dataType" : "string", "isSensitive" : true, "description" : "Not mendatory, your github username and token allows you to use Github API much more, such as checking for skills, updating them etc etc", - "category" : "credentials" + "category" : "credentials", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } }, "aliceUpdateChannel" : { "defaultValue": "master", @@ -548,6 +639,13 @@ "IT", "CH" ] + }, + "pl": { + "default" : false, + "defaultCountryCode": "PL", + "countryCodes" : [ + "PL" + ] } }, "dataType" : "list", @@ -585,6 +683,13 @@ "IT", "CH" ] + }, + "pl": { + "default" : false, + "defaultCountryCode": "PL", + "countryCodes" : [ + "PL" + ] } }, "display" : "hidden", @@ -649,11 +754,58 @@ "description" : "Change the web interface port to be used", "category" : "system" }, + "scenariosActive" : { + "defaultValue": false, + "dataType" : "boolean", + "isSensitive" : false, + "description" : "Activates the scenarios support on the webinterface, using Node-RED.", + "category" : "scenarios", + "onUpdate" : "NodeRedManager.toggle" + }, + "dontStopNodeRed" : { + "defaultValue": false, + "dataType" : "boolean", + "isSensitive" : false, + "description" : "If activated, Node-RED won't be stopped when Alice is shut down.", + "category" : "scenarios", + "parent" : { + "config" : "scenariosActive", + "condition": "is", + "value" : true + } + }, "devMode" : { "defaultValue": false, "dataType" : "boolean", "isSensitive" : false, "description" : "Activates the developer part of the interface, for skill development", "category" : "system" + }, + "suggestSkillsToInstall" : { + "defaultValue": false, + "dataType" : "boolean", + "isSensitive" : false, + "description" : "If enabled, whenever something you say is not recognized, Alice will try to propose a skill that can do it.", + "category" : "system", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } + }, + "internetQuality" : { + "defaultValue": 10, + "dataType" : "range", + "min" : 1, + "max" : 10, + "step" : 1, + "isSensitive" : false, + "description" : "How would you rate your internet connection QUALITY? 0 = drops all the time, 10 = Very stable", + "category" : "system", + "parent" : { + "config" : "stayCompletlyOffline", + "condition": "is", + "value" : false + } } } diff --git a/core/Initializer.py b/core/Initializer.py index 9f7dc5f9d..1cb0c9d77 100644 --- a/core/Initializer.py +++ b/core/Initializer.py @@ -26,7 +26,10 @@ def __init__(self, default: dict): def __getitem__(self, item): try: - return super().__getitem__(item) or '' + item = super().__getitem__(item) + if not item: + raise Exception + return item except: print(f'Missing key **{item}** in provided yaml file.') return '' @@ -83,9 +86,10 @@ def start(self): if not self.confsFile.exists() and self.oldConfFile.exists(): self._logger.logFatal('Found old conf file, trying to migrate...') try: + # noinspection PyPackageRequirements,PyUnresolvedReferences import config.py - self.confsFile.write_text(json.dumps(config.settings, indent=4, ensure_ascii=False, sort_keys=True)) + self.confsFile.write_text(json.dumps(config.settings, indent='\t', ensure_ascii=False, sort_keys=True)) except: self._logger.logFatal('Something went wrong migrating the old configs, aborting') return False @@ -137,17 +141,13 @@ def loadConfig(self) -> dict: try: # noinspection PyUnboundLocalVariable load = yaml.safe_load(f) - if not load: - raise yaml.YAMLError - initConfs = InitDict(load) + # Check that we are running using the latest yaml + if float(initConfs['version']) < VERSION: + self._logger.logFatal('The yaml file you are using is deprecated. Please update it before trying again') + except yaml.YAMLError as e: self._logger.logFatal(f'Failed loading init configurations: {e}') - return dict() - - # Check that we are running using the latest yaml - if float(initConfs['version']) < VERSION: - self._logger.logFatal('The yaml file you are using is deprecated. Please update it before trying again') return initConfs @@ -359,7 +359,7 @@ def initProjectAlice(self) -> bool: # NOSONAR elif not self._confsFile.exists() and self._confsSample.exists(): self._logger.logWarning('No config file found, creating it from sample file') - self._confsFile.write_text(json.dumps({configName: configData['defaultValue'] for configName, configData in json.loads(self._confsSample.read_text()).items()}, indent=4, ensure_ascii=False)) + self._confsFile.write_text(json.dumps({configName: configData['defaultValue'] for configName, configData in json.loads(self._confsSample.read_text()).items()}, indent='\t', ensure_ascii=False)) elif self._confsFile.exists() and initConfs['forceRewrite']: self._logger.logWarning('Config file found and force rewrite specified, let\'s restart all this!') @@ -367,7 +367,7 @@ def initProjectAlice(self) -> bool: # NOSONAR self._logger.logFatal('Unfortunately it won\'t be possible, config sample is not existing') return False - self._confsFile.write_text(json.dumps(self._confsSample.read_text(), indent=4)) + self._confsFile.write_text(json.dumps(self._confsSample.read_text(), indent='\t')) try: @@ -537,6 +537,11 @@ def initProjectAlice(self) -> bool: # NOSONAR audioHardware = hardware break + if not audioHardware: + confs['disableSoundAndMic'] = True + else: + confs['disableSoundAndMic'] = False + hlcServiceFilePath = Path('/etc/systemd/system/hermesledcontrol.service') if initConfs['useHLC']: @@ -656,7 +661,7 @@ def initProjectAlice(self) -> bool: # NOSONAR sort = dict(sorted(confs.items())) try: - self._confsFile.write_text(json.dumps(sort, indent=4)) + self._confsFile.write_text(json.dumps(sort, indent='\t')) except Exception as e: self._logger.logFatal(f'An error occured while writting final configuration file: {e}') diff --git a/core/asr/ASRManager.py b/core/asr/ASRManager.py index 4d8d7638e..2d0236032 100644 --- a/core/asr/ASRManager.py +++ b/core/asr/ASRManager.py @@ -23,6 +23,7 @@ def __init__(self): self._asr = None self._streams: Dict[str, Recorder] = dict() self._translator = Translator() + self._usingFallback = False def onStart(self): @@ -98,7 +99,8 @@ def asr(self) -> Asr: def onInternetConnected(self): - if self.ConfigManager.getAliceConfigByName('stayCompletlyOffline') or self.ConfigManager.getAliceConfigByName('keepASROffline'): + if self.ConfigManager.getAliceConfigByName('stayCompletlyOffline') or self.ConfigManager.getAliceConfigByName('keepASROffline') or \ + self.ConfigManager.getAliceConfigByName('asrFallback') == self.ConfigManager.getAliceConfigByName('asr'): return if not self._asr.isOnlineASR: diff --git a/core/asr/model/GoogleAsr.py b/core/asr/model/GoogleAsr.py index 69b3ab527..189267c1f 100644 --- a/core/asr/model/GoogleAsr.py +++ b/core/asr/model/GoogleAsr.py @@ -1,5 +1,7 @@ import os from pathlib import Path +from threading import Event +from time import time from typing import Generator, Optional from core.asr.model.ASRResult import ASRResult @@ -9,6 +11,7 @@ from core.util.Stopwatch import Stopwatch try: + # noinspection PyUnresolvedReferences,PyPackageRequirements from google.cloud.speech import SpeechClient, enums, types except: pass # Auto installed @@ -34,8 +37,11 @@ def __init__(self): self._client: Optional[SpeechClient] = None self._streamingConfig: Optional[types.StreamingRecognitionConfig] = None - self._previousCapture = '' + self._internetLostFlag = Event() # Set if internet goes down, cut the decoding + self._lastResultCheck = 0 # The time the intermediate results were last checked. If actual time is greater than this value + 3, stop processing, internet issues + self._previousCapture = '' # The text that was last captured in the iteration + self._delayedGoogleConfirmation = False # set whether slow internet is detected or not def onStart(self): super().onStart() @@ -68,6 +74,7 @@ def decodeStream(self, session: DialogSession) -> Optional[ASRResult]: responses = self._client.streaming_recognize(self._streamingConfig, requests) result = self._checkResponses(session, responses) except: + self._internetLostFlag.clear() self.logWarning('Failed ASR request') self.end() @@ -80,11 +87,24 @@ def decodeStream(self, session: DialogSession) -> Optional[ASRResult]: ) if result else None + def onInternetLost(self): + self._internetLostFlag.set() + + def _checkResponses(self, session: DialogSession, responses: Generator) -> Optional[tuple]: if responses is None: return None for response in responses: + if self._internetLostFlag.is_set(): + self.logDebug('Internet connectivity lost during ASR decoding') + + if not response.results: + raise Exception('Internet connectivity lost during decoding') + + result = response.results[0] + return result.alternatives[0].transcript, result.alternatives[0].confidence + if not response.results: continue @@ -93,9 +113,36 @@ def _checkResponses(self, session: DialogSession, responses: Generator) -> Optio continue if result.is_final: + self._lastResultCheck = 0 + self._delayedGoogleConfirmation = False + # print(f'Text confirmed by Google') return result.alternatives[0].transcript, result.alternatives[0].confidence elif result.alternatives[0].transcript != self._previousCapture: self.partialTextCaptured(session=session, text=result.alternatives[0].transcript, likelihood=result.alternatives[0].confidence, seconds=0) - self._previousCapture = result.alternatives[0].transcript + # below function captures the "potential" full utterance not just one word from it + if len(self._previousCapture) <= len(result.alternatives[0].transcript): + self._previousCapture = result.alternatives[0].transcript + elif result.alternatives[0].transcript == self._previousCapture: + + # If we are here it's cause google hasn't responded yet with confirmation on captured text + # Store the time in seconds since epoch + now = int(time()) + # Set a reference to nows time plus 3 seconds + self._lastResultCheck = now + 3 + # wait 3 seconds and see if google responds + if not self._delayedGoogleConfirmation: + # print(f'Text of "{self._previousCapture}" captured but not confirmed by GoogleASR yet') + while now <= self._lastResultCheck: + now = int(time()) + self._delayedGoogleConfirmation = True + # Give google the option to still process the utterance + continue + # During next iteration, If google hasn't responded in 3 seconds assume intent is correct + if self._delayedGoogleConfirmation: + self.logDebug(f'Stopping process as there seems to be connectivity issues') + self._lastResultCheck = 0 + self._delayedGoogleConfirmation = False + return result.alternatives[0].transcript, result.alternatives[0].confidence return None + diff --git a/core/asr/model/PocketSphinxAsr.py b/core/asr/model/PocketSphinxAsr.py index 87b7cc41f..35dfd8252 100644 --- a/core/asr/model/PocketSphinxAsr.py +++ b/core/asr/model/PocketSphinxAsr.py @@ -89,7 +89,7 @@ def downloadLanguage(self, forceLang: str = '') -> bool: return False else: # TODO be universal - self.downloadLanguage(forceLang='eu-US') + self.downloadLanguage(forceLang='en-US') else: if download.suffix == '.tar': dest = Path(venv, 'model', lang.lower()) diff --git a/core/asr/model/SnipsAsr.py b/core/asr/model/SnipsAsr.py index c021e0cc9..558948bc6 100644 --- a/core/asr/model/SnipsAsr.py +++ b/core/asr/model/SnipsAsr.py @@ -18,9 +18,10 @@ class SnipsAsr(Asr): 'system': [ 'libgfortran3' ], - 'pip' : [] + 'pip' : [] } + def __init__(self): super().__init__() self._capableOfArbitraryCapture = True @@ -28,6 +29,12 @@ def __init__(self): self._listening = False + def installDependencies(self): + super().installDependencies() + self.Commons.runRootSystemCommand(['systemctl', 'stop', 'snips-asr']) + self.Commons.runRootSystemCommand(['systemctl', 'disable', 'snips-asr']) + + def onStartListening(self, session): self._listening = True diff --git a/core/base/AssistantManager.py b/core/base/AssistantManager.py index 10a200264..b6e37d4d0 100644 --- a/core/base/AssistantManager.py +++ b/core/base/AssistantManager.py @@ -31,7 +31,14 @@ def clearAssistant(self): def checkAssistant(self, forceRetrain: bool = False): self.logInfo('Checking assistant') - if not self.checkConsistency() or forceRetrain: + + if forceRetrain: + self.logInfo('Forced assistant training') + self.train() + elif not self._assistantPath.exists(): + self.logInfo('Assistant not found') + self.train() + elif not self.checkConsistency(): self.logInfo('Assistant is not consistent, it needs training') self.train() @@ -43,9 +50,6 @@ def checkAssistant(self, forceRetrain: bool = False): def checkConsistency(self) -> bool: - if not self._assistantPath.exists() or not self.DialogTemplateManager.checkData(): - return False - existingIntents: Dict[str, dict] = dict() existingSlots: Dict[str, set] = dict() @@ -196,7 +200,7 @@ def train(self): assistant['intents'] = [intent for intent in intents.values()] - self._assistantPath.write_text(json.dumps(assistant, ensure_ascii=False, indent=4, sort_keys=True)) + self._assistantPath.write_text(json.dumps(assistant, ensure_ascii=False, indent='\t', sort_keys=True)) self.linkAssistant() self.broadcast(method='snipsAssistantInstalled', exceptions=[self.name], propagateToSkills=True) diff --git a/core/base/ConfigManager.py b/core/base/ConfigManager.py index 8ce65615f..f132e6914 100644 --- a/core/base/ConfigManager.py +++ b/core/base/ConfigManager.py @@ -4,6 +4,7 @@ import typing from pathlib import Path +import sounddevice as sd import toml from core.ProjectAliceExceptions import ConfigurationUpdateFailed, VitalConfigMissing @@ -44,6 +45,21 @@ def onStart(self): if conf not in self._aliceConfigurations or self._aliceConfigurations[conf] == '': raise VitalConfigMissing(conf) + for setting, definition in {**self._aliceTemplateConfigurations, **self._skillsTemplateConfigurations}.items(): + function = definition.get('onStart', None) + if function: + try: + if '.' in function: + self.logWarning(f'Use of manager for configuration **onStart** for config "{setting}" is not allowed') + function = function.split('.')[-1] + + func = getattr(self, function) + func() + except AttributeError: + self.logWarning(f'Configuration onStart method **{function}** does not exist') + except Exception as e: + self.logError(f'Configuration onStart method **{function}** failed: {e}') + def _loadCheckAndUpdateAliceConfigFile(self): self.logInfo('Checking Alice configuration file') @@ -57,7 +73,7 @@ def _loadCheckAndUpdateAliceConfigFile(self): if not aliceConfigs: self.logInfo('Creating config file from config template') aliceConfigs = {configName: configData['defaultValue'] if 'defaultValue' in configData else configData for configName, configData in self._aliceTemplateConfigurations.items()} - self.CONFIG_FILE.write_text(json.dumps(aliceConfigs, indent=4, ensure_ascii=False)) + self.CONFIG_FILE.write_text(json.dumps(aliceConfigs, indent='\t', ensure_ascii=False)) changes = False @@ -82,7 +98,7 @@ def _loadCheckAndUpdateAliceConfigFile(self): if setting == 'supportedLanguages': continue - if definition['dataType'] != 'list' and definition['dataType'] != 'longstring': + if definition['dataType'] != 'list' and definition['dataType'] != 'longstring' and 'onInit' not in definition: if not isinstance(aliceConfigs[setting], type(definition['defaultValue'])): changes = True try: @@ -93,14 +109,29 @@ def _loadCheckAndUpdateAliceConfigFile(self): # If casting failed let's fall back to the new default value self.logWarning(f'Existing configuration type missmatch: **{setting}**, replaced with template configuration') aliceConfigs[setting] = definition['defaultValue'] - elif definition['dataType'] == 'list': + elif definition['dataType'] == 'list' and 'onInit' not in definition: values = definition['values'].values() if isinstance(definition['values'], dict) else definition['values'] - if aliceConfigs[setting] not in values: + if aliceConfigs[setting] and aliceConfigs[setting] not in values: changes = True self.logWarning(f'Selected value **{aliceConfigs[setting]}** for setting **{setting}** doesn\'t exist, reverted to default value --{definition["defaultValue"]}--') aliceConfigs[setting] = definition['defaultValue'] + function = definition.get('onInit', None) + if function: + try: + if '.' in function: + self.logWarning(f'Use of manager for configuration **onInit** for config "{setting}" is not allowed') + function = function.split('.')[-1] + + func = getattr(self, function) + func() + except AttributeError: + self.logWarning(f'Configuration onInit method **{function}** does not exist') + except Exception as e: + self.logError(f'Configuration onInit method **{function}** failed: {e}') + + # Setting logger level immediately if aliceConfigs['advancedDebug'] and not aliceConfigs['debug']: aliceConfigs['debug'] = True @@ -122,6 +153,14 @@ def _loadCheckAndUpdateAliceConfigFile(self): self._aliceConfigurations = aliceConfigs + def updateAliceConfigDefinitionValues(self, setting: str, value: typing.Any): + if setting not in self._aliceTemplateConfigurations: + self.logWarning(f'Was asked to update **{setting}** from config templates, but setting doesn\'t exist') + return + + self._aliceTemplateConfigurations[setting]['values'] = value + + @staticmethod def loadJsonFromFile(jsonFile: Path) -> dict: try: @@ -131,13 +170,14 @@ def loadJsonFromFile(jsonFile: Path) -> dict: raise - def updateAliceConfiguration(self, key: str, value: typing.Any): + def updateAliceConfiguration(self, key: str, value: typing.Any, dump: bool = True): """ Updating a core config is sensitive, if the request comes from a skill. First check if the request came from a skill at anytime and if so ask permission to the user :param key: str :param value: str + :param dump: bool If set to False, the configs won't be dumped to the json file :return: None """ @@ -167,8 +207,18 @@ def updateAliceConfiguration(self, key: str, value: typing.Any): self.logWarning(f'Was asked to update **{key}** but key doesn\'t exist') raise ConfigurationUpdateFailed() + pre = self.getAliceConfUpdatePreProcessing(key) + if pre and not self.ConfigManager.doConfigUpdatePreProcessing(pre, value): + return + self._aliceConfigurations[key] = value - self.writeToAliceConfigurationFile() + + if dump: + self.writeToAliceConfigurationFile() + + pp = self.ConfigManager.getAliceConfUpdatePostProcessing(key) + if pp: + self.ConfigManager.doConfigUpdatePostProcessing(pp) def bulkUpdateAliceConfigurations(self): @@ -179,7 +229,7 @@ def bulkUpdateAliceConfigurations(self): if key not in self._aliceConfigurations: self.logWarning(f'Was asked to update **{key}** but key doesn\'t exist') continue - self._aliceConfigurations[key] = value + self.updateAliceConfiguration(key, value, False) self.writeToAliceConfigurationFile() self.deletePendingAliceConfigurationUpdates() @@ -276,7 +326,7 @@ def writeToAliceConfigurationFile(self, confs: dict = None): self._aliceConfigurations = sort try: - self.CONFIG_FILE.write_text(json.dumps(sort, indent=4, sort_keys=True)) + self.CONFIG_FILE.write_text(json.dumps(sort, indent='\t', sort_keys=True)) except Exception: raise ConfigurationUpdateFailed() @@ -293,7 +343,7 @@ def _writeToSkillConfigurationFile(self, skillName: str, confs: dict): confsCleaned = {key: value for key, value in confs.items() if key not in misterProper} skillConfigFile = Path(self.Commons.rootDir(), 'skills', skillName, 'config.json') - skillConfigFile.write_text(json.dumps(confsCleaned, indent=4, ensure_ascii=False, sort_keys=True)) + skillConfigFile.write_text(json.dumps(confsCleaned, indent='\t', ensure_ascii=False, sort_keys=True)) def loadSnipsConfigurations(self) -> dict: @@ -366,6 +416,14 @@ def getAliceConfigByName(self, configName: str) -> typing.Any: return '' + def getAliceConfigTemplateByName(self, configName: str) -> typing.Any: + if configName in self._aliceTemplateConfigurations: + return self._aliceTemplateConfigurations[configName] + else: + self.logDebug(f'Trying to get config template **{configName}** but it does not exist') + return '' + + def getSkillConfigByName(self, skillName: str, configName: str) -> typing.Any: return self._skillsConfigurations.get(skillName, dict()).get(configName, None) @@ -501,7 +559,18 @@ def getAliceConfUpdatePostProcessing(self, confName: str) -> typing.Optional[str def doConfigUpdatePreProcessing(self, function: str, value: typing.Any) -> bool: # Call alice config pre processing functions. try: - func = getattr(self, function) + if '.' in function: + manager, function = function.split('.') + + try: + mngr = getattr(self, manager) + except AttributeError: + self.logWarning(f'Config pre processing manager **{manager}** does not exist') + return False + else: + mngr = self + + func = getattr(mngr, function) except AttributeError: self.logWarning(f'Configuration pre processing method **{function}** does not exist') return False @@ -513,12 +582,27 @@ def doConfigUpdatePreProcessing(self, function: str, value: typing.Any) -> bool: return False - def doConfigUpdatePostProcessing(self, functions: set): + def doConfigUpdatePostProcessing(self, functions: typing.Union[str, set]): # Call alice config post processing functions. This will call methods that are needed after a certain setting was # updated while Project Alice was running + + if isinstance(functions, str): + functions = {functions} + for function in functions: try: - func = getattr(self, function) + if '.' in function: + manager, function = function.split('.') + + try: + mngr = getattr(self, manager) + except AttributeError: + self.logWarning(f'Config post processing manager **{manager}** does not exist') + return False + else: + mngr = self + + func = getattr(mngr, function) except AttributeError: self.logWarning(f'Configuration post processing method **{function}** does not exist') continue @@ -532,9 +616,9 @@ def doConfigUpdatePostProcessing(self, functions: set): def updateMqttSettings(self): self.ConfigManager.updateSnipsConfiguration('snips-common', 'mqtt', f'{self.getAliceConfigByName("mqttHost")}:{self.getAliceConfigByName("mqttPort"):}', False, True) - self.ConfigManager.updateSnipsConfiguration('snips-common', 'mqtt_username', self.getAliceConfigByName('mqttHost'), False, True) - self.ConfigManager.updateSnipsConfiguration('snips-common', 'mqtt_password', self.getAliceConfigByName('mqttHost'), False, True) - self.ConfigManager.updateSnipsConfiguration('snips-common', 'mqtt_tls_cafile', self.getAliceConfigByName('mqttHost'), True, True) + self.ConfigManager.updateSnipsConfiguration('snips-common', 'mqtt_username', self.getAliceConfigByName('mqttUser'), False, True) + self.ConfigManager.updateSnipsConfiguration('snips-common', 'mqtt_password', self.getAliceConfigByName('mqttPassword'), False, True) + self.ConfigManager.updateSnipsConfiguration('snips-common', 'mqtt_tls_cafile', self.getAliceConfigByName('mqttTLSFile'), True, True) self.reconnectMqtt() @@ -617,6 +701,36 @@ def getGithubAuth(self) -> tuple: return (username, token) if (username and token) else None + def populateAudioInputConfig(self): + try: + devices = self._listAudioDevices() + self.updateAliceConfigDefinitionValues(setting='inputDevice', value=devices) + except: + if not self.getAliceConfigByName('disableSoundAndMic'): + self.logWarning('No audio input device found') + + + def populateAudioOutputConfig(self): + try: + devices = self._listAudioDevices() + self.updateAliceConfigDefinitionValues(setting='outputDevice', value=devices) + except: + if not self.getAliceConfigByName('disableSoundAndMic'): + self.logWarning('No audio output device found') + + + @staticmethod + def _listAudioDevices() -> list: + try: + devices = [device['name'] for device in sd.query_devices()] + if not devices: + raise Exception + except: + raise + + return devices + + @property def snipsConfigurations(self) -> dict: return self._snipsConfigurations diff --git a/core/base/SkillManager.py b/core/base/SkillManager.py index fbd3bf897..fe2d62cc0 100644 --- a/core/base/SkillManager.py +++ b/core/base/SkillManager.py @@ -34,7 +34,8 @@ class SkillManager(Manager): DATABASE = { 'skills' : [ 'skillName TEXT NOT NULL UNIQUE', - 'active INTEGER NOT NULL DEFAULT 1' + 'active INTEGER NOT NULL DEFAULT 1', + 'scenarioVersion TEXT NOT NULL DEFAULT "0.0.0"', ], 'widgets': [ 'parent TEXT NOT NULL UNIQUE', @@ -69,6 +70,7 @@ def __init__(self): self._postBootSkillActions = dict() self._widgets = dict() + self._widgetsByIndex = dict() def onStart(self): @@ -95,6 +97,7 @@ def onStart(self): self.ConfigManager.loadCheckAndUpdateSkillConfigurations() self.startAllSkills() + self.sortWidgetZIndexes() # noinspection SqlResolve @@ -173,7 +176,7 @@ def changeSkillStateInDB(self, skillName: str, newState: bool): 'state' : 0, 'posx' : 0, 'posy' : 0, - 'zindex': 9999 + 'zindex': -1 }, row=('parent', skillName) ) @@ -231,21 +234,31 @@ def onAssistantInstalled(self, **kwargs): def sortWidgetZIndexes(self): - widgets = dict() + # Create a list of skills with their z index as key + self._widgetsByIndex = dict() for skillName, widgetList in self._widgets.items(): for widget in widgetList.values(): - widgets[int(widget.zindex)] = widget + if widget.state != 1: + continue + + if int(widget.zindex) not in self._widgetsByIndex: + self._widgetsByIndex[int(widget.zindex)] = widget + else: + i = 1000 + while True: + if i not in self._widgetsByIndex: + self._widgetsByIndex[i] = widget + break + i += 1 + + # Rewrite a logical zindex flow + for i, widget in enumerate(self._widgetsByIndex.values()): + widget.zindex = i + widget.saveToDB() - counter = 0 - for i in sorted(widgets.keys()): - if widgets[i].state == 0: - widgets[i].zindex = -1 - widgets[i].saveToDB() - continue - widgets[i].zindex = counter - counter += 1 - widgets[i].saveToDB() + def nextZIndex(self) -> int: + return len(self._widgetsByIndex) @property @@ -608,7 +621,7 @@ def _checkForSkillInstall(self): root = Path(self.Commons.rootDir(), constants.SKILL_INSTALL_TICKET_PATH) files = [f for f in root.iterdir() if f.suffix == '.install'] - if self._busyInstalling.isSet() or not files or self.ProjectAlice.restart or self.ProjectAlice.updating: + if self._busyInstalling.isSet() or not files or self.ProjectAlice.restart or self.ProjectAlice.updating or self.NluManager.training: return self.logInfo(f'Found {len(files)} install ticket', plural='ticket') @@ -913,6 +926,19 @@ def allScenarioNodes(self) -> Dict[str, tuple]: return ret + def getSkillScenarioVersion(self, skillName: str) -> Version: + if skillName not in self._skillList: + return Version.fromString('0.0.0') + else: + # noinspection SqlResolve + query = 'SELECT * FROM :__table__ WHERE skillName = :skillName' + data = self.DatabaseManager.fetch(tableName='skills', query=query, values={'skillName': skillName}, callerName=self.name) + if not data: + return Version.fromString('0.0.0') + + return Version.fromString(data['scenarioVersion']) + + def wipeSkills(self, addDefaults: bool = True): shutil.rmtree(Path(self.Commons.rootDir(), 'skills')) Path(self.Commons.rootDir(), 'skills').mkdir() @@ -938,21 +964,7 @@ def createNewSkill(self, skillDefinition: dict) -> bool: try: self.logInfo(f'Creating new skill "{skillDefinition["name"]}"') - rootDir = Path(self.Commons.rootDir()) / 'skills' - skillTemplateDir = rootDir / 'skill_DefaultTemplate' - - if skillTemplateDir.exists(): - shutil.rmtree(skillTemplateDir) - - self.Commons.runSystemCommand(['git', '-C', str(rootDir), 'clone', f'{constants.GITHUB_URL}/skill_DefaultTemplate.git']) - skillName = skillDefinition['name'][0].upper() + skillDefinition['name'][1:] - skillDir = rootDir / skillName - - skillTemplateDir.rename(skillDir) - - installFile = skillDir / f'{skillDefinition["name"]}.install' - Path(skillDir, 'DefaultTemplate.install').rename(installFile) supportedLanguages = [ 'en' ] @@ -962,6 +974,8 @@ def createNewSkill(self, skillDefinition: dict) -> bool: supportedLanguages.append('de') if skillDefinition['it'] == 'yes': supportedLanguages.append('it') + if skillDefinition['pl'] == 'yes': + supportedLanguages.append('pl') conditions = { 'lang': supportedLanguages @@ -982,103 +996,28 @@ def createNewSkill(self, skillDefinition: dict) -> bool: if skillDefinition['conditionActiveManager']: conditions['activeManager'] = [manager.strip() for manager in skillDefinition['conditionActiveManager'].split(',')] - installContent = { - 'name' : skillName, - 'version' : '0.0.1', - 'icon' : 'fab fa-battle-net', - 'category' : 'undefined', - 'author' : self.ConfigManager.getAliceConfigByName('githubUsername'), - 'maintainers' : [], - 'desc' : skillDefinition['description'].capitalize(), - 'aliceMinVersion' : constants.VERSION, - 'systemRequirements': [req.strip() for req in skillDefinition['sysreq'].split(',')], - 'pipRequirements' : [req.strip() for req in skillDefinition['pipreq'].split(',')], + data = { + 'username' : self.ConfigManager.getAliceConfigByName('githubUsername'), + 'skillName' : skillName, + 'description' : skillDefinition['description'].capitalize(), + 'category' : skillDefinition['category'], + 'speakableName' : skillDefinition['speakableName'], + 'langs' : supportedLanguages, + 'createInstructions': skillDefinition['instructions'], + 'pipreq' : [req.strip() for req in skillDefinition['pipreq'].split(',')], + 'sysreq' : [req.strip() for req in skillDefinition['sysreq'].split(',')], + 'widgets' : [self.Commons.toPascalCase(widget).strip() for widget in skillDefinition['widgets'].split(',')], + 'scenarioNodes' : [self.Commons.toPascalCase(node).strip() for node in skillDefinition['nodes'].split(',')], + 'outputDestination' : str(Path(self.Commons.rootDir()) / 'skills' / skillName), 'conditions' : conditions } - # Install file - with installFile.open('w') as fp: - fp.write(json.dumps(installContent, indent=4)) - - # Dialog templates and talks - dialogTemplateTemplate = skillDir / 'dialogTemplate/default.json' - with dialogTemplateTemplate.open() as fp: - dialogTemplate = json.load(fp) - dialogTemplate['skill'] = skillName - dialogTemplate['description'] = skillDefinition['description'].capitalize() - - for lang in supportedLanguages: - with Path(skillDir, f'dialogTemplate/{lang}.json').open('w+') as fp: - fp.write(json.dumps(dialogTemplate, indent=4)) - - with Path(skillDir, f'talks/{lang}.json').open('w+') as fp: - fp.write(json.dumps(dict())) + dump = Path(f'/tmp/{skillName}.json') + dump.write_text(json.dumps(data, ensure_ascii=False)) - dialogTemplateTemplate.unlink() - - # Widgets - if skillDefinition['widgets']: - widgetRootDir = skillDir / 'widgets' - css = widgetRootDir / 'css/widget.css' - js = widgetRootDir / 'js/widget.js' - lang = widgetRootDir / 'lang/widget.lang.json' - html = widgetRootDir / 'templates/widget.html' - python = widgetRootDir / 'widget.py' - - for widget in skillDefinition['widgets'].split(','): - widgetName = widget.strip() - widgetName = widgetName[0].upper() + widgetName[1:] - - content = css.read_text().replace('%widgetname%', widgetName) - with Path(widgetRootDir, f'css/{widgetName}.css').open('w+') as fp: - fp.write(content) - - shutil.copy(str(js), str(js).replace('widget.js', f'{widgetName}.js')) - shutil.copy(str(lang), str(lang).replace('widget.lang.json', f'{widgetName}.lang.json')) - - content = html.read_text().replace('%widgetname%', widgetName) - with Path(widgetRootDir, f'templates/{widgetName}.html').open('w+') as fp: - fp.write(content) - - content = python.read_text().replace('Template(Widget)', f'{widgetName}(Widget)') - with Path(widgetRootDir, f'{widgetName}.py').open('w+') as fp: - fp.write(content) - - css.unlink() - js.unlink() - lang.unlink() - html.unlink() - python.unlink() - - else: - shutil.rmtree(str(Path(skillDir, 'widgets'))) - - languages = '' - for lang in supportedLanguages: - languages += f' {lang}\n' - - # Readme file - content = Path(skillDir, 'README.md').read_text().replace('%skillname%', skillName) \ - .replace('%author%', self.ConfigManager.getAliceConfigByName('githubUsername')) \ - .replace('%minVersion%', constants.VERSION) \ - .replace('%description%', skillDefinition['description'].capitalize()) \ - .replace('%languages%', languages) - - with Path(skillDir, 'README.md').open('w') as fp: - fp.write(content) - - # Main class - classFile = skillDir / f'{skillDefinition["name"]}.py' - Path(skillDir, 'DefaultTemplate.py').rename(classFile) - - content = classFile.read_text().replace('%skillname%', skillName) \ - .replace('%author%', self.ConfigManager.getAliceConfigByName('githubUsername')) \ - .replace('%description%', skillDefinition['description'].capitalize()) - - with classFile.open('w') as fp: - fp.write(content) - - self.logInfo(f'Created "{skillDefinition["name"]}" skill') + self.Commons.runSystemCommand(['./venv/bin/pip', '--upgrade', 'projectalice-sk']) + self.Commons.runSystemCommand(['./venv/bin/projectalice-sk', 'create', '--file', f'{str(dump)}']) + self.logInfo(f'Created **skillName** skill') return True except Exception as e: @@ -1117,7 +1056,7 @@ def uploadSkillToGithub(self, skillName: str, skillDesc: str) -> bool: self.Commons.runSystemCommand(['git', '-C', str(localDirectory), 'remote', 'add', 'origin', remote]) self.Commons.runSystemCommand(['git', '-C', str(localDirectory), 'add', '--all']) - self.Commons.runSystemCommand(['git', '-C', str(localDirectory), 'commit', '-m', '"Initial upload"']) + self.Commons.runSystemCommand(['git', '-C', str(localDirectory), 'commit', '-m', '"Initial upload by Project Alice Skill Kit"']) self.Commons.runSystemCommand(['git', '-C', str(localDirectory), 'push', '--set-upstream', 'origin', 'master']) url = f'https://github.com/{self.ConfigManager.getAliceConfigByName("githubUsername")}/skill_{skillName}.git' @@ -1137,6 +1076,8 @@ def downloadInstallTicket(self, skillName: str) -> bool: ): raise Exception + requests.get(f'https://skills.projectalice.ch/{skillName}') + shutil.move(tmpFile.with_suffix('.tmp'), tmpFile) return True except Exception as e: diff --git a/core/base/SkillStoreManager.py b/core/base/SkillStoreManager.py index 4ca26005c..790101343 100644 --- a/core/base/SkillStoreManager.py +++ b/core/base/SkillStoreManager.py @@ -1,3 +1,5 @@ +import difflib +from random import shuffle from typing import Optional import requests @@ -6,14 +8,18 @@ from core.base.model.Manager import Manager from core.base.model.Version import Version from core.commons import constants +from core.dialog.model.DialogSession import DialogSession from core.util.Decorators import Online class SkillStoreManager(Manager): + SUGGESTIONS_DIFF_LIMIT = 0.75 + def __init__(self): super().__init__() self._skillStoreData = dict() + self._skillSamplesData = dict() @property @@ -25,23 +31,51 @@ def onStart(self): self.refreshStoreData() - @Online(catchOnly=True) def onQuarterHour(self): self.refreshStoreData() + @Online(catchOnly=True) def refreshStoreData(self): - updateChannel = self.ConfigManager.getAliceConfigByName('skillsUpdateChannel') - req = requests.get(url=f'https://skills.projectalice.io/assets/store/{updateChannel}.json') + req = requests.get(url=constants.SKILLS_STORE_ASSETS) if req.status_code not in {200, 304}: return self._skillStoreData = req.json() + if not self.ConfigManager.getAliceConfigByName('suggestSkillsToInstall'): + return + + req = requests.get(url=constants.SKILLS_SAMPLES_STORE_ASSETS) + if req.status_code not in {200, 304}: + return + + self.prepareSamplesData(req.json()) + + + def prepareSamplesData(self, data: dict): + if not data: + return + + for skillName, skill in data.items(): + self._skillSamplesData.setdefault(skillName, skill.get(self.LanguageManager.activeLanguage, list())) + def _getSkillUpdateVersion(self, skillName: str) -> Optional[tuple]: + """ + Get the highest skill version number a user can install. + This is based on the user preferences, dependending on the current Alice version + and the user's selected update channel for skills + In case nothing is found, DO NOT FALLBACK TO MASTER + + :param skillName: The skill to look for + :return: tuple + """ versionMapping = self._skillStoreData.get(skillName, dict()).get('versionMapping', dict()) + if not versionMapping: + raise GithubNotFound + userUpdatePref = self.ConfigManager.getAliceConfigByName('skillsUpdateChannel') skillUpdateVersion = (Version(), '') @@ -68,6 +102,52 @@ def _getSkillUpdateVersion(self, skillName: str) -> Optional[tuple]: return skillUpdateVersion + def findSkillSuggestion(self, session: DialogSession, string: str = None) -> set: + suggestions = set() + if not self._skillSamplesData or not self.InternetManager.online: + return suggestions + + userInput = session.input if not string else string + if not userInput: + return suggestions + + for skillName, samples in self._skillSamplesData.items(): + for sample in samples: + diff = difflib.SequenceMatcher(None, userInput, sample).ratio() + if diff >= self.SUGGESTIONS_DIFF_LIMIT: + suggestions.add(skillName) + break + + userInputs = list() + userInput = userInput.split() + + if len(userInput) == 1: + userInputs.append(userInput.copy()) + + for _ in range(max(len(userInput), 8)): + shuffle(userInput) + userInputs.append(userInput.copy()) + + for skillName, samples in self._skillSamplesData.items(): + for sample in samples: + for userInput in userInputs: + diff = difflib.SequenceMatcher(None, userInput, sample).ratio() + if diff >= self.SUGGESTIONS_DIFF_LIMIT: + suggestions.add(skillName) + break + + ret = set() + for suggestedSkillName in suggestions: + speakableName = self._skillStoreData.get(suggestedSkillName, dict()).get('speakableName', '') + + if not speakableName: + continue + + ret.add((suggestedSkillName, speakableName)) + + return ret + + def getSkillUpdateTag(self, skillName: str) -> str: try: return self._getSkillUpdateVersion(skillName)[1] diff --git a/core/base/SuperManager.py b/core/base/SuperManager.py index c1c990447..712747913 100644 --- a/core/base/SuperManager.py +++ b/core/base/SuperManager.py @@ -54,77 +54,87 @@ def __init__(self, mainClass): def onStart(self): - commons = self._managers.pop('CommonsManager') - commons.onStart() - - configManager = self._managers.pop('ConfigManager') - configManager.onStart() - - languageManager = self._managers.pop('LanguageManager') - languageManager.onStart() - - locationManager = self._managers.pop('LocationManager') - locationManager.onStart() - - deviceManager = self._managers.pop('DeviceManager') - deviceManager.onStart() - - audioServer = self._managers.pop('AudioManager') - audioServer.onStart() - - internetManager = self._managers.pop('InternetManager') - internetManager.onStart() - - databaseManager = self._managers.pop('DatabaseManager') - databaseManager.onStart() - - userManager = self._managers.pop('UserManager') - userManager.onStart() - - mqttManager = self._managers.pop('MqttManager') - mqttManager.onStart() - - talkManager = self._managers.pop('TalkManager') - skillManager = self._managers.pop('SkillManager') - assistantManager = self._managers.pop('AssistantManager') - dialogTemplateManager = self._managers.pop('DialogTemplateManager') - nluManager = self._managers.pop('NluManager') - nodeRedManager = self._managers.pop('NodeRedManager') - - for manager in self._managers.values(): - if manager: - manager.onStart() - - talkManager.onStart() - nluManager.onStart() - skillManager.onStart() - dialogTemplateManager.onStart() - assistantManager.onStart() - nodeRedManager.onStart() - - self._managers[configManager.name] = configManager - self._managers[audioServer.name] = audioServer - self._managers[languageManager.name] = languageManager - self._managers[locationManager.name] = locationManager - self._managers[deviceManager.name] = deviceManager - self._managers[talkManager.name] = talkManager - self._managers[databaseManager.name] = databaseManager - self._managers[userManager.name] = userManager - self._managers[mqttManager.name] = mqttManager - self._managers[skillManager.name] = skillManager - self._managers[dialogTemplateManager.name] = dialogTemplateManager - self._managers[assistantManager.name] = assistantManager - self._managers[nluManager.name] = nluManager - self._managers[internetManager.name] = internetManager - self._managers[nodeRedManager.name] = nodeRedManager + try: + commons = self._managers.pop('CommonsManager') + commons.onStart() + + configManager = self._managers.pop('ConfigManager') + configManager.onStart() + + languageManager = self._managers.pop('LanguageManager') + languageManager.onStart() + + locationManager = self._managers.pop('LocationManager') + locationManager.onStart() + + deviceManager = self._managers.pop('DeviceManager') + deviceManager.onStart() + + audioServer = self._managers.pop('AudioManager') + audioServer.onStart() + + internetManager = self._managers.pop('InternetManager') + internetManager.onStart() + + databaseManager = self._managers.pop('DatabaseManager') + databaseManager.onStart() + + userManager = self._managers.pop('UserManager') + userManager.onStart() + + mqttManager = self._managers.pop('MqttManager') + mqttManager.onStart() + + talkManager = self._managers.pop('TalkManager') + skillManager = self._managers.pop('SkillManager') + assistantManager = self._managers.pop('AssistantManager') + dialogTemplateManager = self._managers.pop('DialogTemplateManager') + nluManager = self._managers.pop('NluManager') + nodeRedManager = self._managers.pop('NodeRedManager') + + for manager in self._managers.values(): + if manager: + manager.onStart() + + talkManager.onStart() + nluManager.onStart() + skillManager.onStart() + dialogTemplateManager.onStart() + assistantManager.onStart() + nodeRedManager.onStart() + + self._managers[configManager.name] = configManager + self._managers[audioServer.name] = audioServer + self._managers[languageManager.name] = languageManager + self._managers[locationManager.name] = locationManager + self._managers[deviceManager.name] = deviceManager + self._managers[talkManager.name] = talkManager + self._managers[databaseManager.name] = databaseManager + self._managers[userManager.name] = userManager + self._managers[mqttManager.name] = mqttManager + self._managers[skillManager.name] = skillManager + self._managers[dialogTemplateManager.name] = dialogTemplateManager + self._managers[assistantManager.name] = assistantManager + self._managers[nluManager.name] = nluManager + self._managers[internetManager.name] = internetManager + self._managers[nodeRedManager.name] = nodeRedManager + except Exception as e: + import traceback + + traceback.print_exc() + Logger().logFatal(f'Error while starting managers: {e}') def onBooted(self): self.mqttManager.playSound(soundFilename='boot') - for manager in self._managers.values(): - if manager: - manager.onBooted() + manager = None + try: + for manager in self._managers.values(): + if manager: + manager.onBooted() + except Exception as e: + Logger().logError(f'Error while sending onBooted to manager **{manager.name}**: {e}') @staticmethod @@ -197,8 +207,9 @@ def initManagers(self): def onStop(self): managerName = constants.UNKNOWN_MANAGER - mqttManager = self._managers.pop('MqttManager') try: + mqttManager = self._managers.pop('MqttManager') + for managerName, manager in self._managers.items(): manager.onStop() @@ -206,8 +217,6 @@ def onStop(self): mqttManager.onStop() except Exception as e: Logger().logError(f'Error while shutting down manager **{managerName}**: {e}') - import traceback - traceback.print_exc() def getManager(self, managerName: str): diff --git a/core/base/model/AliceSkill.py b/core/base/model/AliceSkill.py index dde89e250..717b4f76e 100644 --- a/core/base/model/AliceSkill.py +++ b/core/base/model/AliceSkill.py @@ -59,8 +59,8 @@ def __init__(self, supportedIntents: Iterable = None, databaseSchema: dict = Non self._widgets = dict() self._deviceTypes = dict() self._intentsDefinitions = dict() - self._scenarioNodeName = '' - self._scenarioNodeVersion = Version(mainVersion=0, updateVersion=0, hotfix=0) + self._scenarioPackageName = '' + self._scenarioPackageVersion = Version(mainVersion=0, updateVersion=0, hotfix=0) self._supportedIntents: Dict[str, Intent] = self.buildIntentList(supportedIntents) self.loadIntentsDefinition() @@ -89,7 +89,7 @@ def addUtterance(self, text: str, intent: str) -> bool: if not text in utterances: utterances.append(text) data['intents'][i]['utterances'] = utterances - file.write_text(json.dumps(data, ensure_ascii=False, indent=4)) + file.write_text(json.dumps(data, ensure_ascii=False, indent='\t')) return True return False @@ -103,8 +103,8 @@ def loadScenarioNodes(self): try: with path.open('r') as fp: data = json.load(fp) - self._scenarioNodeName = data['name'] - self._scenarioNodeVersion = Version.fromString(data['version']) + self._scenarioPackageName = data['name'] + self._scenarioPackageVersion = Version.fromString(data['version']) except Exception as e: self.logWarning(f'Failed to load scenario nodes: {e}') @@ -402,12 +402,12 @@ def delayed(self, value: bool): @property def scenarioNodeName(self) -> str: - return self._scenarioNodeName + return self._scenarioPackageName @property def scenarioNodeVersion(self) -> Version: - return self._scenarioNodeVersion + return self._scenarioPackageVersion @property @@ -431,7 +431,7 @@ def instructions(self) -> str: def hasScenarioNodes(self) -> bool: - return self._scenarioNodeName != '' + return self._scenarioPackageName != '' def subscribeIntents(self): diff --git a/core/base/model/GithubCloner.py b/core/base/model/GithubCloner.py index 4c4221b06..405d850ca 100644 --- a/core/base/model/GithubCloner.py +++ b/core/base/model/GithubCloner.py @@ -1,6 +1,5 @@ -from pathlib import Path - import shutil +from pathlib import Path from core.base.SuperManager import SuperManager from core.base.model.ProjectAliceObject import ProjectAliceObject @@ -40,12 +39,14 @@ def clone(self, skillName: str) -> bool: def _doClone(self, skillName: str) -> bool: try: + updateTag = self.SkillStoreManager.getSkillUpdateTag(skillName) if not Path(self._dest / '.git').exists(): self.Commons.runSystemCommand(['git', '-C', str(self._dest), 'init']) self.Commons.runSystemCommand(['git', '-C', str(self._dest), 'remote', 'add', 'origin', self._baseUrl]) + self.Commons.runSystemCommand(['git', '-C', str(self._dest), 'pull']) - self.Commons.runSystemCommand(['git', '-C', str(self._dest), 'pull', 'origin', 'master']) - self.Commons.runSystemCommand(['git', '-C', str(self._dest), 'checkout', self.SkillStoreManager.getSkillUpdateTag(skillName)]) + self.Commons.runSystemCommand(['git', '-C', str(self._dest), 'checkout', updateTag]) + self.Commons.runSystemCommand(['git', '-C', str(self._dest), 'pull', 'origin', updateTag]) return True except Exception as e: diff --git a/core/base/model/Manager.py b/core/base/model/Manager.py index f2bc47f8f..567476c34 100644 --- a/core/base/model/Manager.py +++ b/core/base/model/Manager.py @@ -41,11 +41,13 @@ def getFunctionCaller(self) -> Optional[str]: def onStart(self): self.logInfo(f'Starting') + self._isActive = True return self._initDB() def onStop(self): self.logInfo(f'Stopping') + self._isActive = False def _initDB(self): diff --git a/core/base/model/ProjectAliceObject.py b/core/base/model/ProjectAliceObject.py index ac7882c3b..b2cc42a8e 100644 --- a/core/base/model/ProjectAliceObject.py +++ b/core/base/model/ProjectAliceObject.py @@ -87,6 +87,8 @@ def broadcast(self, method: str, exceptions: list = None, manager = None, propag for name in deadManagers: del SM.SuperManager.getInstance().managers[name] + if method == 'onAudioFrame': + return # Now send the event over mqtt payload = dict() @@ -98,7 +100,7 @@ def broadcast(self, method: str, exceptions: list = None, manager = None, propag pass self.MqttManager.publish( - topic=method, + topic=f'projectalice/events/{method}', payload=payload ) @@ -750,3 +752,8 @@ def LocationManager(self): #NOSONAR @property def WakewordManager(self): #NOSONAR return SM.SuperManager.getInstance().wakewordManager + + + @property + def NodeRedManager(self): #NOSONAR + return SM.SuperManager.getInstance().nodeRedManager diff --git a/core/base/model/Widget.py b/core/base/model/Widget.py index 210914802..d036a687f 100644 --- a/core/base/model/Widget.py +++ b/core/base/model/Widget.py @@ -77,7 +77,7 @@ def __init__(self, data: sqlite3.Row): if 'zindex' in data.keys() and data['zindex'] is not None: self._zindex = data['zindex'] else: - self._zindex = 999 + self._zindex = -1 updateWidget = True if updateWidget: diff --git a/core/commons/CommonsManager.py b/core/commons/CommonsManager.py index 1f9f438a9..f22a94d4c 100644 --- a/core/commons/CommonsManager.py +++ b/core/commons/CommonsManager.py @@ -13,6 +13,7 @@ from datetime import datetime from pathlib import Path from typing import Any, Union +from uuid import UUID import requests from googletrans import Translator @@ -335,6 +336,15 @@ def randomNumber(self, length: int) -> int: return int(number) if not number.startswith('0') else self.randomNumber(length) + @staticmethod + def isUuid(uuid: str) -> bool: + try: + _ = UUID(uuid) + return True + except ValueError: + return False + + # noinspection PyUnusedLocal def py_error_handler(filename, line, function, err, fmt): #NOSONAR # Errors are handled by our loggers diff --git a/core/commons/constants.py b/core/commons/constants.py index c70c99acb..cb3bc1c02 100644 --- a/core/commons/constants.py +++ b/core/commons/constants.py @@ -1,4 +1,4 @@ -VERSION = '1.0.0-b3' +VERSION = '1.0.0-b4' DEFAULT = 'default' UNKNOWN_WORD = 'unknownword' @@ -15,6 +15,8 @@ GITHUB_RAW_URL = 'https://raw.githubusercontent.com/project-alice-assistant' GITHUB_API_URL = 'https://api.github.com/repos/project-alice-assistant' SKILL_REDIRECT_URL = 'https://skills.projectalice.ch' +SKILLS_STORE_ASSETS = 'https://skills.projectalice.io/assets/store/skills.json' +SKILLS_SAMPLES_STORE_ASSETS = 'https://skills.projectalice.io/assets/store/skills.samples' GITHUB_REPOSITORY_ID = 193512918 JSON_EXT = '.json' diff --git a/core/device/DeviceManager.py b/core/device/DeviceManager.py index a8cf57016..16de50973 100644 --- a/core/device/DeviceManager.py +++ b/core/device/DeviceManager.py @@ -555,7 +555,7 @@ def getAliceTypeDevices(self, connectedOnly: bool = False, includeMain: bool = F def getAliceTypeDeviceTypeIds(self): - return [self.getMainDevice().deviceTypeID, self.getDeviceTypeByName(self.SAT_TYPE)] + return [self.getMainDevice().deviceTypeID, self.getDeviceTypeByName(self.SAT_TYPE).id] def siteIdToDeviceName(self, siteId: str) -> str: diff --git a/core/device/LocationManager.py b/core/device/LocationManager.py index 843923806..fb157952b 100644 --- a/core/device/LocationManager.py +++ b/core/device/LocationManager.py @@ -63,6 +63,10 @@ def deleteLocation(self, locId: int) -> bool: callerName=self.name, values={'id': locId}) self._locations.pop(locId, None) + + self.DatabaseManager.delete(tableName=self.DeviceManager.DB_LINKS, + callerName=self.DeviceManager.name, + values={'locationID': locId}) return True diff --git a/core/dialog/DialogTemplateManager.py b/core/dialog/DialogTemplateManager.py index efab84e8b..4b7afab20 100644 --- a/core/dialog/DialogTemplateManager.py +++ b/core/dialog/DialogTemplateManager.py @@ -165,7 +165,7 @@ def buildCache(self): for file in pathToResources.glob(f'*{constants.JSON_EXT}'): cached[skillName][file.stem] = self.Commons.fileChecksum(file) - self._pathToChecksums.write_text(json.dumps(cached, indent=4, sort_keys=True)) + self._pathToChecksums.write_text(json.dumps(cached, indent='\t', sort_keys=True)) def cleanCache(self, skillName: str): @@ -176,7 +176,7 @@ def cleanCache(self, skillName: str): checksums = json.load(self._pathToChecksums) checksums.pop(skillName, None) - self._pathToChecksums.write_text(json.dumps(checksums, indent=4, sort_keys=True)) + self._pathToChecksums.write_text(json.dumps(checksums, indent='\t', sort_keys=True)) def clearCache(self, rebuild: bool = True): @@ -193,7 +193,7 @@ def addUtterance(self, session: DialogSession): if not text: return - intent = session.previousIntent + intent = session.secondLastIntent if not intent: return diff --git a/core/dialog/model/DialogSession.py b/core/dialog/model/DialogSession.py index 9cb7e3487..a59da28c2 100644 --- a/core/dialog/model/DialogSession.py +++ b/core/dialog/model/DialogSession.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any +from typing import Any, Optional from paho.mqtt.client import MQTTMessage @@ -101,5 +101,16 @@ def addToHistory(self, intent: Intent): @property - def previousIntent(self) -> str: - return str(self.intentHistory[-1]) + def previousIntent(self) -> Optional[str]: + try: + return str(self.intentHistory[-1]) + except: + return None + + + @property + def secondLastIntent(self) -> Optional[str]: + try: + return str(self.intentHistory[-2]) + except: + return None diff --git a/core/interface/NodeRedManager.py b/core/interface/NodeRedManager.py index 9e411fa6c..2fec477ae 100644 --- a/core/interface/NodeRedManager.py +++ b/core/interface/NodeRedManager.py @@ -1,25 +1,113 @@ import json +import os +import shutil +import time from pathlib import Path +from subprocess import PIPE, Popen from core.base.model.Manager import Manager -from core.base.model.Version import Version class NodeRedManager(Manager): + PACKAGE_PATH = Path('../.node-red/package.json') + DEFAULT_NODES_ACTIVE = { + 'node-red': [ + 'debug', + 'JSON', + 'split', + 'sort', + 'function', + 'change' + ] + } def __init__(self): super().__init__() def onStart(self): + self.isActive = self.ConfigManager.getAliceConfigByName('scenariosActive') + + if not self.isActive: + return + super().onStart() + + if not self.PACKAGE_PATH.exists(): + self.isActive = False + self.ThreadManager.newThread(name='installNodered', target=self.install) + return + self.injectSkillNodes() self.Commons.runRootSystemCommand(['systemctl', 'start', 'nodered']) + def install(self): + self.logInfo('Node-RED not found, installing, this might take a while...') + self.Commons.downloadFile( + url='https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered', + dest='var/cache/node-red.sh' + ) + self.Commons.runRootSystemCommand('chmod +x var/cache/node-red.sh'.split()) + + process = Popen('./var/cache/node-red.sh'.split(), stdin=PIPE, stdout=PIPE) + try: + process.stdin.write(b'y\n') + process.stdin.write(b'n\n') + except IOError: + self.logError('Failed installing Node-RED') + self.onStop() + return + + process.stdin.close() + returnCode = process.wait(timeout=900) + + if returnCode: + self.logError('Failed installing Node-red') + self.onStop() + else: + self.logInfo('Succesfully installed Node-red') + self.configureNewNodeRed() + self.onStart() + + + def configureNewNodeRed(self): + self.logInfo('Configuring') + # Start to generate base configs and stop it after + self.Commons.runRootSystemCommand(['systemctl', 'start', 'nodered']) + time.sleep(5) + self.Commons.runRootSystemCommand(['systemctl', 'stop', 'nodered']) + time.sleep(3) + + config = Path(self.PACKAGE_PATH.parent, '.config.nodes.json') + data = json.loads(config.read_text()) + for package in data.values(): + keeper = self.DEFAULT_NODES_ACTIVE.get(package['name'], list()) + for node in package['nodes'].values(): + if node['name'] in keeper: + continue + node['enabled'] = False + + config.write_text(json.dumps(data)) + self.logInfo('Nodes configured') + self.logInfo('Applying Project Alice settings') + + self.Commons.runSystemCommand('npm install --prefix ~/.node-red @node-red-contrib-themes/midnight-red'.split()) + shutil.copy(Path('system/node-red/settings.js'), Path(os.path.expanduser('~/.node-red'), 'settings.js')) + self.logInfo("All done, let's start all this") + + def onStop(self): super().onStop() - self.Commons.runRootSystemCommand(['systemctl', 'stop', 'nodered']) + if not self.ConfigManager.getAliceConfigByName('dontStopNodeRed'): + self.Commons.runRootSystemCommand(['systemctl', 'stop', 'nodered']) + + + def toggle(self): + if self.isActive: + self.onStop() + else: + self.onStart() def reloadServer(self): @@ -27,28 +115,43 @@ def reloadServer(self): def injectSkillNodes(self): - package = Path('../.node-red/package.json') - if not package.exists(): - #self.logWarning('Package json file for Node Red is missing. Is Node Red even installed?') + restart = False + + if not self.PACKAGE_PATH.exists(): + self.logWarning('Package json file for Node-RED is missing. Is Node-RED even installed?') + self.onStop() return for skillName, tup in self.SkillManager.allScenarioNodes().items(): scenarioNodeName, scenarioNodeVersion, scenarioNodePath = tup path = Path('../.node-red/node_modules', scenarioNodeName, 'package.json') if not path.exists(): - self.logInfo('New scenario node found') + self.logInfo(f'New scenario node found for skill **{skillName}**: {scenarioNodeName}') install = self.Commons.runSystemCommand(f'cd ~/.node-red && npm install {scenarioNodePath}', shell=True) if install.returncode == 1: - self.logWarning(f'Something went wrong installing new node: {install.stderr}') - + self.logWarning(f'Something went wrong installing new node **{scenarioNodeName}**: {install.stderr}') + else: + restart = True continue - with path.open('r') as fp: - data = json.load(fp) - version = Version.fromString(data['version']) + version = self.SkillManager.getSkillScenarioVersion(skillName) - if version < scenarioNodeVersion: - self.logInfo('New scenario node update found') - install = self.Commons.runSystemCommand(f'cd ~/.node-red && npm install {scenarioNodePath}', shell=True) - if install.returncode == 1: - self.logWarning(f'Something went wrong updating node: {install.stderr}') + if version < scenarioNodeVersion: + self.logInfo(f'New scenario update found for node **{scenarioNodeName}**') + install = self.Commons.runSystemCommand(f'cd ~/.node-red && npm install {scenarioNodePath}', shell=True) + if install.returncode == 1: + self.logWarning(f'Something went wrong updating node **{scenarioNodeName}**: {install.stderr}') + continue + + self.DatabaseManager.update( + tableName='skills', + callerName=self.SkillManager.name, + values={ + 'scenarioVersion': str(scenarioNodeVersion) + }, + row=('skillName', skillName) + ) + restart = True + + if restart: + self.reloadServer() diff --git a/core/interface/WebInterfaceManager.py b/core/interface/WebInterfaceManager.py index dfd37122f..c66b82fba 100644 --- a/core/interface/WebInterfaceManager.py +++ b/core/interface/WebInterfaceManager.py @@ -119,7 +119,7 @@ def onStart(self): # 'ssl_context' : 'adhoc', 'debug' : self.ConfigManager.getAliceConfigByName('debug'), 'port' : int(self.ConfigManager.getAliceConfigByName('webInterfacePort')), - 'host' : self.Commons.getLocalIp(), + 'host' : '0.0.0.0', 'use_reloader': False } ) diff --git a/core/interface/languages/de.json b/core/interface/languages/de.json index 1b19a0bbe..7390806c7 100644 --- a/core/interface/languages/de.json +++ b/core/interface/languages/de.json @@ -15,46 +15,47 @@ "updatee": "aktualisieren", "addWidgetTitle": "Verfügbare Widgets", "viewIntents": "Intents ansehen", - "skillSettings": "Skilleinstellungen", - "save": "Speichern", - "followLogs": "Autoscroll", - "adminTabTitleSetting": "Alice Einstellungen", - "adminTabTitleUtilities": "Utilities", - "restartAlice": "Alice neustarten", - "reboot": "Reboot", - "loading": "Laden...", - "trainAssistant": "Assistant trainieren", - "wipeAll": "Alles löschen", - "devmodeNewSkill": "Neuer Skill", - "devmodeEditSkill": "Skill editieren", - "reload": "Neu laden", - "skillName": "Skill Name", - "skillDesc": "Skill Beschreibung", - "skillLanguage": "Skill Sprache", - "english": "Englisch", - "french": "Französisch", - "german": "Deutsch", - "italian": "Italienisch", - "pipreq": "PIP Anforderungen", - "sysreq": "System Anforderungen", - "conditions": "Bedingungen", - "conditionOnline": "Benötigt eine bestehende Internetverbindung", - "conditionSkill": "Benötigt weitere Skills", - "conditionNotSkill": "Skills die nicht parallel installiert sein dürfen", - "conditionASRArbitrary": "Dieser Skill benötigt Wörter, auf welche die Spracherkennung nicht trainiert wurde", - "conditionActiveManager": "Die gelisteten Manager werden benötigt ", - "widgets": "Widgets", + "skillSettings" : "Skilleinstellungen", + "save" : "Speichern", + "followLogs" : "Autoscroll", + "adminTabTitleSetting" : "Alice Einstellungen", + "adminTabTitleUtilities" : "Utilities", + "restartAlice" : "Alice neustarten", + "reboot" : "Reboot", + "loading" : "Laden...", + "trainAssistant" : "Assistant trainieren", + "wipeAll" : "Alles löschen", + "devmodeNewSkill" : "Neuer Skill", + "devmodeEditSkill" : "Skill editieren", + "reload" : "Neu laden", + "skillName" : "Skill Name", + "skillDesc" : "Skill Beschreibung", + "skillLanguage" : "Skill Sprache", + "english" : "Englisch", + "french" : "Französisch", + "german" : "Deutsch", + "italian" : "Italienisch", + "polish" : "Polish", + "pipreq" : "PIP Anforderungen", + "sysreq" : "System Anforderungen", + "conditions" : "Bedingungen", + "conditionOnline" : "Benötigt eine bestehende Internetverbindung", + "conditionSkill" : "Benötigt weitere Skills", + "conditionNotSkill" : "Skills die nicht parallel installiert sein dürfen", + "conditionASRArbitrary" : "Dieser Skill benötigt Wörter, auf welche die Spracherkennung nicht trainiert wurde", + "conditionActiveManager" : "Die gelisteten Manager werden benötigt ", + "widgets" : "Widgets", "commaSeparatedExplanation": "Einzelne Elemente mit einem Komma trennen, zum Beispiel 'foo, bar, baz'", - "createSkill" : "Erstelle den Skill", - "uploadSkillToGithub" : "Den Skill auf Github hochladen", - "resetSkill" : "Reset", - "actions" : "Actions", - "addUser" : "Neuer Benutzer", - "addWakeword" : "Neues Wakeword", - "tuneWakeword" : "Tune wakeword", - "widgetCustStyleTitle" : "Anzeige Einstellungen", - "widgetConfigTitle" : "Widget Einstellungen", - "noConfs" : "Keine Einstellungen", + "createSkill" : "Erstelle den Skill", + "uploadSkillToGithub" : "Den Skill auf Github hochladen", + "resetSkill" : "Reset", + "actions" : "Actions", + "addUser" : "Neuer Benutzer", + "addWakeword" : "Neues Wakeword", + "tuneWakeword" : "Tune wakeword", + "widgetCustStyleTitle" : "Anzeige Einstellungen", + "widgetConfigTitle" : "Widget Einstellungen", + "noConfs" : "Keine Einstellungen", "saving" : "Speichern", "saved" : "Gespeichert", "saveFailed" : "Speichern fehlgeschlagen", @@ -68,5 +69,9 @@ "renameLocation" : "Wie lautet der neue Name der Lokation?", "warningSkillChangeAliceconf": "Änderung der Systemeinstellungen durch Skills erkannt", "accept" : "akzeptieren", - "refuse" : "ablehnen" + "refuse" : "ablehnen", + "leaveEmptyIfNotNeeded" : "Leave empty is not needed", + "scenarioNodes" : "Scenario nodes", + "category" : "Category", + "speakableName" : "Speakable name" } diff --git a/core/interface/languages/en.json b/core/interface/languages/en.json index 480dc6b1c..5a8c65a93 100644 --- a/core/interface/languages/en.json +++ b/core/interface/languages/en.json @@ -15,46 +15,47 @@ "updatee": "Update", "addWidgetTitle": "Available widgets", "viewIntents": "View intents", - "skillSettings": "Skill settings", - "save": "Save", - "followLogs": "Autoscroll", - "adminTabTitleSetting": "Alice Settings", - "adminTabTitleUtilities": "Utilities", - "restartAlice": "Restart Alice", - "reboot": "Reboot", - "trainAssistant": "Train assistant", - "wipeAll": "Wipe all", - "devmodeNewSkill": "Skill creation", - "devmodeEditSkill": "Skill edition", - "reload": "Reload", - "loading": "Loading...", - "skillName": "Skill name", - "skillDesc": "Skill description", - "skillLanguage": "Skill language", - "english": "English", - "french": "French", - "german": "German", - "italian": "Italian", - "pipreq": "PIP requirements", - "sysreq": "System requirements", - "conditions": "Conditions", - "conditionOnline": "Requires to be connected to the internet", - "conditionSkill": "Required skills", - "conditionNotSkill": "Skills that can't be installed at the same time", - "conditionASRArbitrary": "This skill requires the ASR to understand words that were not trained", - "conditionActiveManager": "Managers listed are required to be running", - "widgets": "Widgets", + "skillSettings" : "Skill settings", + "save" : "Save", + "followLogs" : "Autoscroll", + "adminTabTitleSetting" : "Alice Settings", + "adminTabTitleUtilities" : "Utilities", + "restartAlice" : "Restart Alice", + "reboot" : "Reboot", + "trainAssistant" : "Train assistant", + "wipeAll" : "Wipe all", + "devmodeNewSkill" : "Skill creation", + "devmodeEditSkill" : "Skill edition", + "reload" : "Reload", + "loading" : "Loading...", + "skillName" : "Skill name", + "skillDesc" : "Skill description", + "skillLanguage" : "Skill language", + "english" : "English", + "french" : "French", + "german" : "German", + "italian" : "Italian", + "polish" : "Polish", + "pipreq" : "PIP requirements", + "sysreq" : "System requirements", + "conditions" : "Conditions", + "conditionOnline" : "Requires to be connected to the internet", + "conditionSkill" : "Required skills", + "conditionNotSkill" : "Skills that can't be installed at the same time", + "conditionASRArbitrary" : "This skill requires the ASR to understand words that were not trained", + "conditionActiveManager" : "Managers listed are required to be running", + "widgets" : "Widgets", "commaSeparatedExplanation": "Enter a list separated by comma, example 'foo, bar, baz'", - "createSkill" : "Create the skill", - "uploadSkillToGithub" : "Upload the skill to Github", - "resetSkill" : "Reset", - "actions" : "Actions", - "addUser" : "Add user", - "addWakeword" : "Add wakeword", - "tuneWakeword" : "Tune wakeword", - "widgetCustStyleTitle" : "Display Options", - "widgetConfigTitle" : "Widget Options", - "noConfs" : "No config for this widget!", + "createSkill" : "Create the skill", + "uploadSkillToGithub" : "Upload the skill to Github", + "resetSkill" : "Reset", + "actions" : "Actions", + "addUser" : "Add user", + "addWakeword" : "Add wakeword", + "tuneWakeword" : "Tune wakeword", + "widgetCustStyleTitle" : "Display Options", + "widgetConfigTitle" : "Widget Options", + "noConfs" : "No config for this widget!", "saving" : "Saving", "saved" : "Saved", "saveFailed" : "Save failed", @@ -68,5 +69,9 @@ "renameLocation" : "What is the locations new name?", "warningSkillChangeAliceconf": "Core configuration change attempt detected!", "accept" : "accept", - "refuse" : "refuse" + "refuse" : "refuse", + "leaveEmptyIfNotNeeded" : "Leave empty is not needed", + "scenarioNodes" : "Scenario nodes", + "category" : "Category", + "speakableName" : "Speakable name" } diff --git a/core/interface/languages/fr.json b/core/interface/languages/fr.json index 5f98f7962..3722d4340 100644 --- a/core/interface/languages/fr.json +++ b/core/interface/languages/fr.json @@ -15,46 +15,47 @@ "updatee": "mise à jour", "addWidgetTitle": "Widgets disponibles", "viewIntents": "Voir intents", - "skillSettings": "Configuration", - "save": "Sauvegarder", - "followLogs": "Autoscroll", - "adminTabTitleSetting": "Parametres Alice", - "adminTabTitleUtilities": "Utilitaires", - "restartAlice": "Relancer Alice", - "reboot": "Reboot", - "trainAssistant": "Entrainer assistant", - "wipeAll": "Tout supprimer", - "devmodeNewSkill": "Nouveau skill", - "devmodeEditSkill": "Edition de skill", - "reload": "Recharger", - "loading": "Chargement...", - "skillName": "Nom du skill", - "skillDesc": "Description du skill", - "skillLanguage": "Langue du skill", - "english": "Anglais", - "french": "Français", - "german": "Allemand", - "italian": "Italien", - "pipreq": "Dépendances PIP", - "sysreq": "Dépendance systeme", - "conditions": "Conditions", - "conditionOnline": "Connexion à internet requise", - "conditionSkill": "Skills requis", - "conditionNotSkill": "Skills qui ne doivent pas être installé en même temps", - "conditionASRArbitrary": "Ce skill a besoin d'un ASR qui comprend des mots pour lesquels il n'a pas été entrainé", - "conditionActiveManager": "Managers qui doivent être actifs pour que ce skill fonctionne", - "widgets": "Widgets", + "skillSettings" : "Configuration", + "save" : "Sauvegarder", + "followLogs" : "Autoscroll", + "adminTabTitleSetting" : "Parametres Alice", + "adminTabTitleUtilities" : "Utilitaires", + "restartAlice" : "Relancer Alice", + "reboot" : "Reboot", + "trainAssistant" : "Entrainer assistant", + "wipeAll" : "Tout supprimer", + "devmodeNewSkill" : "Nouveau skill", + "devmodeEditSkill" : "Edition de skill", + "reload" : "Recharger", + "loading" : "Chargement...", + "skillName" : "Nom du skill", + "skillDesc" : "Description du skill", + "skillLanguage" : "Langue du skill", + "english" : "Anglais", + "french" : "Français", + "german" : "Allemand", + "italian" : "Italien", + "polish" : "Polonais", + "pipreq" : "Dépendances PIP", + "sysreq" : "Dépendance systeme", + "conditions" : "Conditions", + "conditionOnline" : "Connexion à internet requise", + "conditionSkill" : "Skills requis", + "conditionNotSkill" : "Skills qui ne doivent pas être installé en même temps", + "conditionASRArbitrary" : "Ce skill a besoin d'un ASR qui comprend des mots pour lesquels il n'a pas été entrainé", + "conditionActiveManager" : "Managers qui doivent être actifs pour que ce skill fonctionne", + "widgets" : "Widgets", "commaSeparatedExplanation": "Une liste séparée par des virgules, exemple 'foo, bar, baz'", - "createSkill" : "Créer le skill", - "uploadSkillToGithub" : "Uploader le skill sur Github", - "resetSkill" : "Reset", - "actions" : "Actions", - "addUser" : "Ajouter utilisateur", - "addWakeword" : "Ajouter wakeword", - "tuneWakeword" : "Tuner wakeword", - "widgetCustStyleTitle" : "Options d'affichage", - "widgetConfigTitle" : "Options de widget", - "noConfs" : "Pas de config pour ce widget!", + "createSkill" : "Créer le skill", + "uploadSkillToGithub" : "Uploader le skill sur Github", + "resetSkill" : "Reset", + "actions" : "Actions", + "addUser" : "Ajouter utilisateur", + "addWakeword" : "Ajouter wakeword", + "tuneWakeword" : "Tuner wakeword", + "widgetCustStyleTitle" : "Options d'affichage", + "widgetConfigTitle" : "Options de widget", + "noConfs" : "Pas de config pour ce widget!", "saving" : "Sauvegarde", "saved" : "Sauvegardé", "saveFailed" : "Erreur de sauvegarde", @@ -68,5 +69,9 @@ "renameLocation" : "Quel est le nouveau nom de l'emplacement?", "warningSkillChangeAliceconf": "Modification de configuration de base demandée par un ou des skill(s)", "accept" : "accepter", - "refuse" : "refuser" + "refuse" : "refuser", + "leaveEmptyIfNotNeeded" : "Leave empty is not needed", + "scenarioNodes" : "Scenario nodes", + "category" : "Category", + "speakableName" : "Nom prononçable" } diff --git a/core/interface/languages/it.json b/core/interface/languages/it.json index f2c172fb2..230ff3fb0 100644 --- a/core/interface/languages/it.json +++ b/core/interface/languages/it.json @@ -15,46 +15,47 @@ "updatee": "aggiorna", "addWidgetTitle": "Widgets disponibili", "viewIntents": "Visualizza intents", - "skillSettings": "Impostazioni skills", - "save": "Save", - "followLogs": "Autoscroll", - "adminTabTitleSetting": "Impostazioni Alice", - "adminTabTitleUtilities": "Utilities", - "restartAlice": "Riavvia Alice", - "reboot": "Riavvia il Sistema", - "trainAndDlAssistant": "Scarica Assistente", - "wipeAll": "Cancella tutto", - "devmodeNewSkill": "Crea skill", - "devmodeEditSkill": "Modifica skill", - "reload": "Ricarica", - "loading": "Caricamento...", - "skillName": "nome Skill", - "skillDesc": "descrizione skill", - "skillLanguage": "Lingua skill", - "english": "Inglese", - "french": "Francese", - "german": "Tedesco", - "italian": "Italiano", - "pipreq": "Prerequisiti di PIP", - "sysreq": "Prerequisiti di sistema", - "conditions": "Condizioni", - "conditionOnline": "Richiede una connessione ad internet", - "conditionSkill": "Skills richieste", - "conditionNotSkill": "Skills che non possono essere installate contemporaneamente", - "conditionASRArbitrary": "Questa skill contiene parole che l'ASR non è addestrato a riconoscere", - "conditionActiveManager": "Devono essere avviati i seguenti gestori", - "widgets": "Widgets", + "skillSettings" : "Impostazioni skills", + "save" : "Save", + "followLogs" : "Autoscroll", + "adminTabTitleSetting" : "Impostazioni Alice", + "adminTabTitleUtilities" : "Utilities", + "restartAlice" : "Riavvia Alice", + "reboot" : "Riavvia il Sistema", + "trainAndDlAssistant" : "Scarica Assistente", + "wipeAll" : "Cancella tutto", + "devmodeNewSkill" : "Crea skill", + "devmodeEditSkill" : "Modifica skill", + "reload" : "Ricarica", + "loading" : "Caricamento...", + "skillName" : "nome Skill", + "skillDesc" : "descrizione skill", + "skillLanguage" : "Lingua skill", + "english" : "Inglese", + "french" : "Francese", + "german" : "Tedesco", + "italian" : "Italiano", + "polish" : "Polish", + "pipreq" : "Prerequisiti di PIP", + "sysreq" : "Prerequisiti di sistema", + "conditions" : "Condizioni", + "conditionOnline" : "Richiede una connessione ad internet", + "conditionSkill" : "Skills richieste", + "conditionNotSkill" : "Skills che non possono essere installate contemporaneamente", + "conditionASRArbitrary" : "Questa skill contiene parole che l'ASR non è addestrato a riconoscere", + "conditionActiveManager" : "Devono essere avviati i seguenti gestori", + "widgets" : "Widgets", "commaSeparatedExplanation": "Una lista separata da virgole, per esempio 'foo, bar, baz'", - "createSkill" : "Crea la skill", - "uploadSkillToGithub" : "Carica la skill su Github", - "resetSkill" : "Resetta", - "actions" : "Azioni", - "addUser" : "Nuovo utente", - "addWakeword" : "Aggiungi wakeword", - "tuneWakeword" : "Regola wakeword", - "widgetCustStyleTitle" : "Opzioni dello schermo", - "widgetConfigTitle" : "Opzioni dei widget", - "noConfs" : "Nessuna configurazione per questo widget!", + "createSkill" : "Crea la skill", + "uploadSkillToGithub" : "Carica la skill su Github", + "resetSkill" : "Resetta", + "actions" : "Azioni", + "addUser" : "Nuovo utente", + "addWakeword" : "Aggiungi wakeword", + "tuneWakeword" : "Regola wakeword", + "widgetCustStyleTitle" : "Opzioni dello schermo", + "widgetConfigTitle" : "Opzioni dei widget", + "noConfs" : "Nessuna configurazione per questo widget!", "saving" : "Salvataggio in corso", "saved" : "Salvato", "saveFailed" : "Salvataggio fallito", @@ -68,5 +69,9 @@ "renameLocation" : "Qual è il nuovo nome per la posizione?", "warningSkillChangeAliceconf": "Some skill(s) want to change some core configurations", "accept" : "accettare", - "refuse" : "refiutare" + "refuse" : "refiutare", + "leaveEmptyIfNotNeeded" : "Leave empty is not needed", + "scenarioNodes" : "Scenario nodes", + "category" : "Category", + "speakableName" : "Speakable name" } diff --git a/core/interface/languages/pl.json b/core/interface/languages/pl.json new file mode 100644 index 000000000..3f3052f3f --- /dev/null +++ b/core/interface/languages/pl.json @@ -0,0 +1,73 @@ +{ + "home": "start", + "skills": "umiejętności", + "myHome": "mój dom", + "scenarios": "scenariusze", + "syslog": "log systemowy", + "alicewatch": "alicewatch", + "admin": "admin", + "devMode": "tryb dev", + "by": "przez", + "status": "status", + "enable": "włącz", + "disable": "wyłącz", + "delete": "usuń", + "updatee": "Aktualizuj", + "addWidgetTitle": "Dostępne widżety", + "viewIntents": "Zobacz intencje", + "skillSettings": "Skill settings", + "save": "Zapisz", + "followLogs": "Auto-przewijanie", + "adminTabTitleSetting": "Ustawienia", + "adminTabTitleUtilities": "Narzędzia", + "restartAlice": "Uruchom ponownie Alice", + "reboot": "Uruchom ponownie", + "trainAssistant": "Trenuj asystenta", + "wipeAll": "Usuń wszystko", + "devmodeNewSkill": "Utwórz umiejętność", + "devmodeEditSkill": "Edycja umijejętności", + "reload": "Przeładuj", + "loading": "Ładowanie...", + "skillName": "Nazwa umiejętności", + "skillDesc": "Opis umiejętności", + "skillLanguage": "Język umiejętności", + "english": "Angielski", + "french": "Francuski", + "german": "Niemiecki", + "italian": "Włoski", + "polish": "Polski", + "pipreq": "Wymagania PIP", + "sysreq": "Wymagania systempwe", + "conditions": "Warunki", + "conditionOnline": "Wymaga połączenia z internetem", + "conditionSkill": "Wymagane umiejętności", + "conditionNotSkill": "Umiejętności, które nie moga być instalowane w tym samym czasie", + "conditionASRArbitrary": "Ta umiejętność wymaga od modułu ASR zrozumienia słów, które nie były trenowane", + "conditionActiveManager": "Menedżerowie z listy poniżej muszą być uruchomieni", + "widgets": "Widżety", + "commaSeparatedExplanation": "Wprowadź listę rozdzieloną przecinkami, np. 'jeden, dwa, trzy'", + "createSkill" : "Utwórz umiejętność", + "uploadSkillToGithub" : "Wyślij umiejętność na Github", + "resetSkill" : "Reset", + "actions" : "Akcje", + "addUser" : "Dodaj użytkownika", + "addWakeword" : "Dodaj wyrażenie wywołujące", + "tuneWakeword" : "Popraw wyrażenie wywołujące", + "widgetCustStyleTitle" : "Wyświetl opcje", + "widgetConfigTitle" : "Opcje widżetu", + "noConfs" : "Brak konfiguracji widżetu!", + "saving" : "Zapisywanie", + "saved" : "Zapisano", + "saveFailed" : "Błąd zapisu", + "confMoveDevice" : "Czy chcesz umieścić to urządzenie w nowej lokalizacji: ", + "confDeleteDevice" : "Czy na pewno chcesz usunąć to urządzenie?", + "confDeleteZone" : "Czy na pewno chcesz usunąć tę lokalizację?", + "nameNewZone" : "Podaj nazwę lokalizacji", + "unreachable" : "Projekt Alice jest obecnie niedostępny,\nstrona spróbuje się wkrótce przeładować", + "instructions" : "Instrukcja", + "renameDevice" : "Podaj nową nazwę urządzenia?", + "renameLocation" : "Podaj nową nazwę lokalizacji?", + "warningSkillChangeAliceconf": "Wykryto próbę zmiany podstawowej konfiguracji!", + "accept" : "zaakceptuj", + "refuse" : "odmów" +} diff --git a/core/interface/static/css/myHome.css b/core/interface/static/css/myHome.css index dea613d47..366a03a9b 100644 --- a/core/interface/static/css/myHome.css +++ b/core/interface/static/css/myHome.css @@ -1,4 +1,5 @@ -.floorPlan-Zone { overflow: visible; background-color: var(--mainBG); color: var(--windowBG); position: absolute; display: flex; justify-content: center; align-items: center; font-size: 1.5em; } +.floorPlan-Zone { overflow: visible; background-color: var(--mainBG); color: var(--windowBG); position: absolute; display: flex; justify-content: center; align-items: center;} +.floorPlan-Zone > .inputOrText {font-size: 1.5em;} .floorPlan-Wall { background-color: var(--accent); } diff --git a/core/interface/static/css/projectalice.css b/core/interface/static/css/projectalice.css index 21d66942b..f2eef6c3a 100644 --- a/core/interface/static/css/projectalice.css +++ b/core/interface/static/css/projectalice.css @@ -94,26 +94,36 @@ button.button{ font-size: 1.6em; height: 2em; } .pageTitle { font-size: 2em; margin-left: 1em; align-items: center; display: flex; width: 7.4em;} .aliceStatus { flex-grow: 1; } + .updateChannelMarker { font-style: italic; margin: 0 .9em; align-items: center; display: flex; } .resourceUsage { margin: 0 .9em; align-items: center; display: flex; } -.page { flex: 1 100%; flex-flow: row nowrap; display: flex; } +.page { flex: 1 100%; flex-flow: row nowrap; display: flex; } .toolbar { background: var(--mainBG); display: flex; float: right; clear: both; padding-top: .2em; padding-left: 1em; border: solid 2px; border-bottom-left-radius: 1.5em; border-top: none; border-color: var(--accent); } + .toolbarButton { font-size: 2em; padding-right: .5em; min-width: .9em; text-align: center; } -.zindexer { position: absolute; top: 15px; right: 15px; color: var(--accent); font-weight: bold; font-size: 1.3em; z-index: 999; cursor: pointer; } -[class^='zindexer-']:hover { color: var(--hover); } +.zindexer { position: absolute; top: 5px; left: 5px; background-color: var(--mainBG); width: 36px; border-radius: 18px; text-align: center; color: var(--text); z-index: 99; } + +.zindexer .clickable:first-child { margin-bottom: 5px; } + +.clickable { cursor: pointer; } + +.clickable:hover { color: var(--hover); } /* Utility screen for administration */ .utility { width: 12em; height: 12em; flex-flow: column; align-items: center; } + .utilityIcon { text-align: center; font-size: 5em; margin-top: .4em; width: 100%; } + .utilityText { font-size: 1.25em; margin: .3em; text-align: center; width: 90%; } /* console and logging */ /* using color markings as defined above */ .console { position: absolute; top: 20px; bottom: 40px; left: 20px; right: 20px; padding: 1em; background-color: black; color: var(--text); overflow-y: scroll; overflow-x: hidden; font-family: var(--readable); -moz-user-select: text; -webkit-touch-callout: default; -webkit-user-select: text; -ms-user-select: text; user-select: text; -khtml-user-select: text; } + .log { display: inline-block; } .logLine { display: flex; font-family: var(--readable); font-size: 0.9em; margin: 2px 0 2px 0; align-items: flex-start; } @@ -127,7 +137,8 @@ button.button{ font-size: 1.6em; height: 2em; } .addWidgetCheck { float: right; color: var(--text); margin-right: .4em; } /* To use with custom Widgets! */ -.widget { display: block; box-shadow: 0 0 20px 5px #141414; background-color: var(--windowBG); cursor: default; overflow: hidden; } +.widget { display: block; box-shadow: 0 0 20px 5px #141414; background-color: var(--windowBG); cursor: default; overflow: hidden; font-family: var(--readable) } + .widgetIcons { font-size: 1.5em; display: flex; align-items: flex-start; } .widgetIconsHidden .widgetIcons { display: none; } .widgetIcon { display: flex; flex-grow: 1; } @@ -140,33 +151,41 @@ button.button{ font-size: 1.6em; height: 2em; } .tabsContainer { padding-bottom: .1em; border: 2px solid var(--secondary); } .tabsContainer ul { display: flex; background-color: var(--secondary); margin-top: 0; padding-left: .4em; } .tabsContainer li { background-color: var(--windowBG); font-size: 1.25em; padding: .2em .7em; margin: .3em .3em 0 0; display: inline-flex; align-items: center; box-sizing: border-box; border-radius: .3em .3em 0 0; border: 2px solid var(--accent); border-bottom: 0; } -.tabsContainer .activeTab { background-color: var(--mainBG); } -.tabsContent { display: flex; overflow-y: auto; overflow-x: hidden; padding-left: .7em; } +.tabsContainer .activeTab { background-color: var(--mainBG); } + +.tabsContent { display: flex; overflow-y: auto; overflow-x: hidden; padding-left: .7em; padding-right: .7em; padding-bottom: .7em; } .skillStoreSkillInfoContainer { margin-top: .4em; display: flex; width: 100%; flex-flow: row; } .skillStoreSkillLeft { flex: 1 } .skillStoreSkillRight { display: flex; margin-right: 5px; align-items: center; justify-content: flex-end; font-size: 3.5em; } -.skillStoreSkillAuthor,.skillStoreSkillVersion,.skillStoreSkillCategory,.skillVersion, -.spaced{ margin: .2em; } +.skillStoreSkillAuthor, .skillStoreSkillVersion, .skillStoreSkillCategory, .skillVersion, +.spaced { margin: .2em; font-size: 0.8em; } /* bigger icon for FA */ .spaced i { width: 1.2em; } /* Tile structure */ .tileContainer { margin: .2em; overflow-y: auto; overflow-x: hidden; display: flex; flex-wrap: wrap; align-content: flex-start; flex-direction: row; } + .tile { background-color: var(--windowBG); margin: .6em; display: flex; flex-wrap: wrap; align-content: flex-start; position: relative; box-shadow: 0 0 20px 5px #141414; } -.skillStoreSkillTile { width: 18.75em; height: 18.75em; } -.skillTile { width: 18.75em; height: 13em; } -.skillStoreSkillDescription { background-color: var(--secondary); font-family: var(--long); font-size: 0.75em; width: 100%; height: 10em; margin: .5em; padding: .5em; box-sizing: border-box; overflow-x: hidden; overflow-y: auto; } +.skillStoreSkillTile { width: 18.75em; height: 18.75em; font-family: var(--readable); } + +.skillTile { width: 18.75em; height: 13em; font-family: var(--readable); font-size: 1em; } + +.skillStoreSkillDescription { background-color: var(--secondary); font-family: var(--long); font-size: 0.75em; width: 100%; height: 10em; margin: .5em; padding: .5em; box-sizing: border-box; overflow-x: hidden; overflow-y: auto; } .skillContainer { position: relative; flex-wrap: wrap; align-content: flex-start; height: 11.3em; margin: .2em; } + .skillDefaultView { display: flex; } + .skillIntentsView { width: 100%; overflow-x: hidden; overflow-y: auto; } -.skillTitle { background-color: var(--secondary); height: 1.5em; width: 100%; padding: 0 .4em; display: flex; align-items: center; justify-content: space-between; overflow: hidden; } +.skillTitle { background-color: var(--secondary); height: 1.5em; width: 100%; padding: 0 .4em; display: flex; align-items: center; justify-content: space-between; overflow: hidden; font-family: var(--short)} + .skillStatus { width: 100%; margin-top: 10px; margin-left: 4px; } + .skillName { overflow: hidden; font-size: 1.25em; } .skillViewIntents, @@ -174,9 +193,11 @@ button.button{ font-size: 1.6em; height: 2em; } .skillInstructions { width: 100%; margin: .2em; text-align: right; } /* Configuration Screens */ -.configBox { width: 100%; display: table; font-family: var(--long); background-color: var(--secondary); border-spacing: 0 10px; } +.configBox { width: 100%; font-family: var(--readable); background-color: var(--secondary); border-spacing: 0 10px; } + .configCategory { width: 100%; display: table; margin-bottom: 30px; font-family: var(--long); background-color: var(--secondary); border-spacing: 0 10px; } -.configLine { width: 100%; display: table-row; } + +.configLine { width: 100%; display: table-row; font-family: var(--readable); } .configLine input, select, textarea { width: 500px; } diff --git a/core/interface/static/js/admin.js b/core/interface/static/js/admin.js index 21019bc88..be43d7810 100644 --- a/core/interface/static/js/admin.js +++ b/core/interface/static/js/admin.js @@ -37,7 +37,7 @@ $(function () { let $icon = $div.children('.utilityIcon').children('i'); $icon.addClass('fa-spin red'); $.ajax({ - url: '/admin/' + endpoint + '/', + url : '/admin/' + endpoint + '/', type: 'POST' }); setTimeout(function () { @@ -45,42 +45,42 @@ $(function () { }, timeout); } - $('#restart').on('click touchstart', function () { + $('#restart').on('click touch', function () { handleUtilityClick($(this), 'restart', 5000); return false; }); - $('#reboot').on('click touchstart', function () { + $('#reboot').on('click touch', function () { handleUtilityClick($(this), 'reboot', 10000); return false; }); - $('#trainAssistant').on('click touchstart', function () { + $('#trainAssistant').on('click touch', function () { handleUtilityClick($(this), 'trainAssistant', 5000); return false; }); - $('#wipeAll').on('click touchstart', function () { + $('#wipeAll').on('click touch', function () { handleUtilityClick($(this), 'wipeAll', 5000); return false; }); - $('#update').on('click touchstart', function () { + $('#update').on('click touch', function () { handleUtilityClick($(this), 'updatee', 5000); return false; }); - $('#addUser').on('click touchstart', function () { + $('#addUser').on('click touch', function () { handleUtilityClick($(this), 'addUser', 1000); return false; }); - $('#addWakeword').on('click touchstart', function () { + $('#addWakeword').on('click touch', function () { handleUtilityClick($(this), 'addWakeword', 1000); return false; }); - $('#tuneWakeword').on('click touchstart', function () { + $('#tuneWakeword').on('click touch', function () { handleUtilityClick($(this), 'tuneWakeword', 1000); return false; }); diff --git a/core/interface/static/js/adminAuth.js b/core/interface/static/js/adminAuth.js index 9429c19bb..7d6951ad2 100644 --- a/core/interface/static/js/adminAuth.js +++ b/core/interface/static/js/adminAuth.js @@ -23,7 +23,7 @@ $(function () { $('#username').on('keyup', function () { $.ajax({ - url: '/adminAuth/login/', + url : '/adminAuth/login/', data: { 'username': $(this).val() }, @@ -31,7 +31,7 @@ $(function () { }); }); - $('.adminAuthKeyboardKey').not('.erase').not('.backspace').on('click touchstart', function () { + $('.adminAuthKeyboardKey').not('.erase').not('.backspace').on('click touch', function () { if (!keyboardAuthNotified) { $.post('/adminAuth/keyboardAuth/'); keyboardAuthNotified = true; @@ -64,16 +64,16 @@ $(function () { return true; }); - $('.erase').on('click touchstart', function () { + $('.erase').on('click touch', function () { code = ''; $('#codeContainer').children('.adminAuthDisplayDigit').each(function () { $(this).removeClass('adminAuthDigitFilled'); }); }); - $('.backspace').on('click touchstart', function () { + $('.backspace').on('click touch', function () { code = code.slice(0, -1); - $('#codeContainer').children('.adminAuthDigitFilled').last().removeClass('adminAuthDigitFilled') + $('#codeContainer').children('.adminAuthDigitFilled').last().removeClass('adminAuthDigitFilled'); }); $('#adminAuthKeyboardContainer').hide(); diff --git a/core/interface/static/js/alicewatch.js b/core/interface/static/js/alicewatch.js index dd505c388..78c4b33c3 100644 --- a/core/interface/static/js/alicewatch.js +++ b/core/interface/static/js/alicewatch.js @@ -55,23 +55,23 @@ $(function () { '[' + time + '] [AliceWatch]Watching on ' + MQTT_HOST + ':' + MQTT_PORT + ' (MQTT)' ); - MQTT.subscribe('projectalice/logging/alicewatch') + MQTT.subscribe('projectalice/logging/alicewatch'); } - $stopScroll.on('click touchstart', function () { + $stopScroll.on('click touch', function () { $(this).hide(); $startScroll.show(); return false; }); - $startScroll.on('click touchstart', function () { + $startScroll.on('click touch', function () { $(this).hide(); $stopScroll.show(); return false; }); let $thermometers = $('[class^="fas fa-thermometer"]'); - $thermometers.on('click touchstart', function () { + $thermometers.on('click touch', function () { $('[class^="fas fa-thermometer"]').removeClass('active'); $(this).addClass('active'); let level = $(this).data('verbosity'); diff --git a/core/interface/static/js/common.js b/core/interface/static/js/common.js index 304f14dd4..545143219 100644 --- a/core/interface/static/js/common.js +++ b/core/interface/static/js/common.js @@ -3,13 +3,13 @@ let mqttSubscribers = {}; let MQTT_HOST; let MQTT_PORT; let LAST_CORE_HEARTBEAT = 0; -let MAIN_GOING_DOWN = false +let MAIN_GOING_DOWN = false; function getCookie(cname) { let name = cname + '='; let decodedCookie = decodeURIComponent(document.cookie); let ca = decodedCookie.split(';'); - for(let i = 0; i < ca.length; i++) { + for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1); @@ -33,57 +33,15 @@ function mqttRegisterSelf(target, method) { mqttSubscribers[method].push(target); } -function initIndexers($element) { - let indexer = $element.children('.zindexer'); - - indexer.children('.zindexer-up').on('click touchscreen', function () { - let $parent = $(this).parent().parent(); - let actualIndex = $element.css('z-index'); - if (actualIndex == null || actualIndex == 'auto') { - actualIndex = 0; - } else { - actualIndex = parseInt(actualIndex); - } - - let baseClass = $parent.attr('class').split(/\s+/)[0]; - $('.' + baseClass).each(function() { - let thisIndex = $(this).css('z-index'); - if (thisIndex != null && thisIndex != 'auto' && parseInt(thisIndex) == actualIndex + 1) { - $(this).css('z-index', actualIndex); - $parent.css('z-index', actualIndex + 1); - return false; - } - }); - }); - - indexer.children('.zindexer-down').on('click touchscreen', function () { - let $parent = $(this).parent().parent(); - let actualIndex = $element.css('z-index'); - if (actualIndex == null || actualIndex == 'auto' || parseInt(actualIndex) <= 0) { - actualIndex = 0; - } else { - actualIndex = parseInt(actualIndex); - } - - let baseClass = $parent.attr('class').split(/\s+/)[0]; - $('.' + baseClass).each(function() { - let thisIndex = $(this).css('z-index'); - if (thisIndex != null && thisIndex != 'auto' && parseInt(thisIndex) == actualIndex -1) { - $(this).css('z-index', actualIndex); - $parent.css('z-index', actualIndex - 1); - return false; - } - }); - }); -} - $(document).tooltip(); $(function () { function onFailure(_msg) { console.log('Mqtt connection failed, retry in 5 seconds'); - setTimeout(function() { connectMqtt(); }, 5000); + setTimeout(function () { + connectMqtt(); + }, 5000); } function onConnect(msg) { @@ -96,7 +54,7 @@ $(function () { } function onConnectionLost(resObj) { - console.log('Mqtt disconnected, automatic reconnect is enabled Error code: ' + resObj.errorCode +' - '+ resObj.errorMessage ); + console.log('Mqtt disconnected, automatic reconnect is enabled Error code: ' + resObj.errorCode + ' - ' + resObj.errorMessage); connectMqtt(); } @@ -123,7 +81,7 @@ $(function () { MQTT_HOST = window.location.hostname; } let randomNum = Math.floor((Math.random() * 10000000) + 1); - MQTT = new Paho.MQTT.Client(MQTT_HOST, MQTT_PORT, 'ProjectAliceInterface'+randomNum); + MQTT = new Paho.MQTT.Client(MQTT_HOST, MQTT_PORT, 'ProjectAliceInterface' + randomNum); MQTT.onMessageArrived = onMessage; MQTT.onConnectionLost = onConnectionLost; MQTT.connect({ @@ -132,12 +90,16 @@ $(function () { timeout : 5 }); } else { - console.log('Failed fetching MQTT settings') - setTimeout(function() { connectMqtt(); }, 5000); + console.log('Failed fetching MQTT settings'); + setTimeout(function () { + connectMqtt(); + }, 5000); } }).fail(function () { - console.log("Coulnd't get MQTT information") - setTimeout(function() { connectMqtt(); }, 5000); + console.log('Coulnd\'t get MQTT information'); + setTimeout(function () { + connectMqtt(); + }, 5000); }); } @@ -171,17 +133,14 @@ $(function () { } else if (payload.status == 'done') { $container.text('Nlu training done!'); } - } - else if (msg.topic == 'projectalice/skills/instructions') { + } else if (msg.topic == 'projectalice/skills/instructions') { let payload = JSON.parse(msg.payloadString); $('#skillInstructions').show(); let $content = $('#skillInstructionsContent'); $content.html($content.html() + payload['instructions']); - } - else if (msg.topic == 'projectalice/devices/coreHeartbeat') { + } else if (msg.topic == 'projectalice/devices/coreHeartbeat') { LAST_CORE_HEARTBEAT = Date.now(); - } - else if (msg.topic == 'projectalice/skills/coreConfigUpdateWarning') { + } else if (msg.topic == 'projectalice/skills/coreConfigUpdateWarning') { $('#serverUnavailable').hide(); let $nodal = $('#coreConfigUpdateAlert'); $nodal.show(); @@ -190,12 +149,12 @@ $(function () { let $container = $('#overlaySkillContent'); if ($container.children().length <= 0) { - $container.append('

' + payload['skill'] + '
' + payload['key'] + ' => ' + payload['value'] + '
'); + $container.append('
' + payload['skill'] + '
' + payload['key'] + ' => ' + payload['value'] + '
'); } else { - let $skillWarning = $('#confWarning_' + payload["skill"]); + let $skillWarning = $('#confWarning_' + payload['skill']); if ($skillWarning.length == 0) { - $container.append('
' + payload['skill'] + '
' + payload['key'] + ' => ' + payload['value'] + '
'); + $container.append('
' + payload['skill'] + '
' + payload['key'] + ' => ' + payload['value'] + '
'); } else { $skillWarning.append('
' + payload['key'] + ' => ' + payload['value'] + '
'); } @@ -237,13 +196,12 @@ $(function () { $('.tabsContent').children().each(function () { if ($(this).attr('id') == $defaultTab.data('for')) { $(this).show(); - } - else { + } else { $(this).hide(); } }); - $('.tab').on('click touchstart', function () { + $('.tab').on('click touch', function () { let target = $(this).data('for'); $(this).addClass('activeTab'); @@ -256,25 +214,24 @@ $(function () { $('.tabsContent').children().each(function () { if ($(this).attr('id') == target) { $(this).show(); - } - else { + } else { $(this).hide(); } }); return false; }); - $('.overlayInfoClose').on('click touchstart', function () { + $('.overlayInfoClose').on('click touch', function () { $(this).parent().hide(); }); - $('#refuseAliceConfUpdate').on('click touchstart', function() { + $('#refuseAliceConfUpdate').on('click touch', function () { $.post('/admin/refuseAliceConfigUpdate/').done(function () { $('#coreConfigUpdateAlert').hide(); }); }); - $('#acceptAliceConfUpdate').on('click touchstart', function() { + $('#acceptAliceConfUpdate').on('click touch', function () { $.post('/admin/acceptAliceConfigUpdate/').done(function () { $('#coreConfigUpdateAlert').hide(); }); @@ -287,5 +244,77 @@ $(function () { setInterval(checkCoreStatus, 2000); - $(":checkbox").checkToggler(); + let $checkboxes = $(':checkbox'); + if ($checkboxes.length) { + $checkboxes.checkToggler(); + } + + // Z-indexers + let $zindexed = $('.z-indexed'); + if ($zindexed.length) { + zIndexMe($zindexed); + } }); + +function reorder($arrow, direction) { + let $widget = $arrow.parent().parent(); + let $container = $widget.parent(); + let $family = $container.children('.z-indexed'); + + let actualIndex = $widget.css('z-index'); + try { + actualIndex = parseInt(actualIndex); + } catch { + actualIndex = 0; + } + + let toIndex; + if (direction === 'down') { + toIndex = Math.max(0, actualIndex - 1); + } else { + let highest = 0; + $family.each(function () { + try { + if (parseInt($(this).css('z-index')) > highest) { + highest = parseInt($(this).css('z-index')); + } + } catch { + return true; + } + }); + toIndex = actualIndex + 1; + if (toIndex > highest) { + return; + } + } + + $family.each(function () { + try { + if ($(this) != $widget && parseInt($(this).css('z-index')) == toIndex) { + $(this).css('z-index', actualIndex); + } + } catch { + return true; + } + }); + $widget.css('z-index', toIndex); +} + + +function zIndexMe($zindexed) { + let $indexUp = $('
'); + let $indexDown = $('
'); + + $indexUp.on('click touch', function () { + reorder($(this), 'up'); + }); + $indexDown.on('click touch', function () { + reorder($(this), 'down'); + }); + + let $zindexer = $('
'); + $zindexer.append($indexUp); + $zindexer.append($indexDown); + + $zindexed.append($zindexer); +} diff --git a/core/interface/static/js/devmode.js b/core/interface/static/js/devmode.js index 3decec6c3..ffcd30daf 100644 --- a/core/interface/static/js/devmode.js +++ b/core/interface/static/js/devmode.js @@ -8,7 +8,7 @@ $(function () { let $goGithubButton = $('#goGithubButton'); function toggleCreateButton() { - if ($('#skillNameOk').is(':visible') && $('#skillDescOk').is(':visible')) { + if ($('#skillNameOk').is(':visible') && $('#skillDescOk').is(':visible') && $('#speakableNameNameOk').is(':visible')) { $createSkillButton.show(); } else { $createSkillButton.hide(); @@ -22,6 +22,8 @@ $(function () { $('#skillNameKo').show(); $('#skillDescOk').hide(); $('#skillDescKo').show(); + $('#speakableNameDescOk').hide(); + $('#speakableNameDescKo').show(); $uploadSkillButton.hide(); $goGithubButton.hide(); } @@ -35,7 +37,7 @@ $(function () { } }); - $('.tab').on('click touchstart', function () { + $('.tab').on('click touch', function () { let target = $(this).data('for'); $(this).addClass('activeTab'); @@ -80,6 +82,17 @@ $(function () { }); }); + $('#speakableName').on('input', function () { + if ($(this).val().length < 5) { + $('#speakableNameNameOk').hide(); + $('#speakableNameNameKo').show(); + } else { + $('#speakableNameNameOk').show(); + $('#speakableNameNameKo').hide(); + } + toggleCreateButton(); + }); + $('#skillDesc').on('input', function () { if ($(this).val().length > 20) { $('#skillDescKo').hide(); @@ -91,11 +104,12 @@ $(function () { toggleCreateButton(); }); - $createSkillButton.on('click touchstart', function () { + $createSkillButton.on('click touch', function () { $.ajax({ - url: '/devmode/' + $skillName.val() + '/', + url : '/devmode/' + $skillName.val() + '/', type: 'PUT', data: { + 'speakableName' : $('#speakableName').val(), 'description' : $('#skillDesc').val(), 'fr' : ($('#fr').is(':checked')) ? 'yes' : 'no', 'de' : ($('#de').is(':checked')) ? 'yes' : 'no', @@ -107,7 +121,7 @@ $(function () { 'conditionSkill' : $('#conditionSkill').val(), 'conditionNotSkill' : $('#conditionNotSkill').val(), 'conditionActiveManager': $('#conditionActiveManager').val(), - 'widgets': $('#widgets').val() + 'widgets' : $('#widgets').val() } }).done(function () { $('#newSkillForm :input').prop('disabled', true); @@ -117,9 +131,9 @@ $(function () { }); }); - $uploadSkillButton.on('click touchstart', function () { + $uploadSkillButton.on('click touch', function () { $.ajax({ - url: '/devmode/uploadToGithub/', + url : '/devmode/uploadToGithub/', type: 'POST', data: { 'skillName': $('#skillName').val(), @@ -133,11 +147,11 @@ $(function () { }); }); - $('#resetSkillButton').on('click touchstart', function () { + $('#resetSkillButton').on('click touch', function () { resetSkillPage(); }); - $goGithubButton.on('click touchstart', function () { + $goGithubButton.on('click touch', function () { window.open($(this).text()); }); @@ -147,7 +161,7 @@ $(function () { } }); - $('[id*=editSkill_]').on('click touchstart', function () { + $('[id*=editSkill_]').on('click touch', function () { window.location.href = '/devmode/editskill/' + $(this).data('skill'); }); }); diff --git a/core/interface/static/js/myHome.js b/core/interface/static/js/myHome.js index 7e55f8cfe..eee922620 100644 --- a/core/interface/static/js/myHome.js +++ b/core/interface/static/js/myHome.js @@ -191,7 +191,7 @@ $(function () { return result; } - $('#backupMyHome').on('click touchstart', function () { + $('#backupMyHome').on('click touch', function () { let data = saveHouse(); let file = new Blob([data], {type: 'application/json'}); let url = window.URL.createObjectURL(file); @@ -201,7 +201,7 @@ $(function () { document.getElementById('downloadBackup').click(); window.URL.revokeObjectURL(url); $link.remove(); - }) + }); // Basic functionality for build area function matrixToAngle(matrix) { @@ -369,31 +369,28 @@ $(function () { data['display']['z-index'] = maxZindex; maxZindex++; } - let $newZone = $('
' + '
' + data['name'] + '
' + - '
' + - '
' + - '
' + '
' + '
'); - initIndexers($newZone); + zIndexMe($newZone); - $newZone.on('click touchstart', function () { + $newZone.on('click touch', function () { if (buildingMode) { if (selectedConstruction == null || selectedConstruction == '') { let wallData = { - 'x' : 50, - 'y' : 50, - 'width' : 25, - 'height': 75, + 'x' : 50, + 'y' : 50, + 'width' : 25, + 'height' : 75, 'rotation': 0 - } + }; let wall = newWall($newZone, wallData); makeResizableRotatableAndDraggable(wall); } @@ -478,23 +475,23 @@ $(function () { content += data['name']; content += ""; content += "
"; - content += "
"; - content += "
"; - content += "
Synonyms:
"; - content += "
"; + content += '
'; + content += '
'; + content += '
Synonyms:
'; + content += '
'; // content += "
Devices:
"; // content += "
Linked Devices:
"; - content += "
"; + content += '
'; $sideBar.html(content); - $('#renameLocation').on('click touchstart', function () { - let targetName = $(this).parent().children('#content') + $('#renameLocation').on('click touch', function () { + let targetName = $(this).parent().children('#content'); let newLocName = prompt($('#langRenameLocation').text(), data['name']); - if(newLocName === null){ + if (newLocName === null) { return; } - $.post('/myhome/Location/'+data['id']+'/saveCoreSettings', {name : newLocName}).done(function(ret) { + $.post('/myhome/Location/' + data['id'] + '/saveCoreSettings', {name: newLocName}).done(function (ret) { if (handleError(ret)) { return; } @@ -509,25 +506,25 @@ $(function () { $sideBar.sidebar({side: 'right'}).trigger('sidebar:open'); // reroute enter to click event - $('.configInput').on('keydown',function (e) { - if (e.key == 'Enter') { - $(this).parent().children('.configListAdd').trigger('click'); - return false; - } + $('.configInput').on('keydown', function (e) { + if (e.key == 'Enter') { + $(this).parent().children('.configListAdd').trigger('click'); + return false; + } }); // add new entry to conf. List - $('.configListAdd').on('click touchstart',function() { + $('.configListAdd').on('click touch', function () { let $parent = $(this).parent(); let $inp = $parent.children('.configInput'); $parent = $parent.parent(); if ($inp.val() != '') { - $.post( '/myhome/'+$parent[0].id, - { value: $inp.val() } ) - .done(function( result ) { - if(handleError(result)){ - return; - } + $.post('/myhome/' + $parent[0].id, + {value: $inp.val()}) + .done(function (result) { + if (handleError(result)) { + return; + } newConfigListVal($parent,$inp.val(),$parent.data('dellink') ); $inp.val(''); }); @@ -568,13 +565,13 @@ $(function () { } function newWall($element, data) { - data = snapPosition(data) + data = snapPosition(data); data = snapAngle(data); let $newWall = $('
' + '
'); - $newWall.on('click touchstart', function () { + $newWall.on('click touch', function () { return false; }); @@ -590,7 +587,7 @@ $(function () { } function newConstruction($element, data) { - data = snapPosition(data) + data = snapPosition(data); data = snapAngle(data); // noinspection CssUnknownTarget let $newConstruction = $('
' + '
'); - $newConstruction.on('click touchstart', function () { + $newConstruction.on('click touch', function () { return false; }); @@ -614,7 +611,7 @@ $(function () { } function newDeco($element, data) { - data = snapPosition(data) + data = snapPosition(data); data = snapAngle(data); // noinspection CssUnknownTarget let $newDeco = $('
' + '
'); - $newDeco.on('click touchstart', function () { + $newDeco.on('click touch', function () { return false; }); @@ -638,35 +635,35 @@ $(function () { } function newDevice($element, data) { - data = snapPosition(data) + data = snapPosition(data); data = snapAngle(data); // noinspection CssUnknownTarget - let $newDevice = $('
' + + let $newDevice = $('
' + '
'); - $newDevice.on('click touchstart', function () { - if(deviceSettingsMode) { - let content = "
"; + $newDevice.on('click touch', function () { + if (deviceSettingsMode) { + let content = '
'; content += data['name']; - content += "
"; - content += "

" + data['deviceType'] + "

"; + content += '
'; + content += '

' + data['deviceType'] + '

'; if (data['uid'] == 'undefined' || data['uid'] == null) { - content += "NO DEVICE PAIRED!
Search Device
" + content += 'NO DEVICE PAIRED!
Search Device
'; } else { - content += "
" + data['uid'] + "
"; + content += '
' + data['uid'] + '
'; } $sideBar.html(content); - $('#startPair').on('click touchstart', function () { - $(this).addClass('waiting') + $('#startPair').on('click touch', function () { + $(this).addClass('waiting'); $.post('Device/' + data['id'] + '/pair').done(function (dataa) { if (handleError(dataa)) { return; } - let sp = $('#startPair') + let sp = $('#startPair'); sp.removeClass('waiting'); sp.hide(); }); @@ -696,9 +693,9 @@ $(function () { $sideBar.append(content); saveEnter(); - $('#SetForm').on('change', function() { + $('#SetForm').on('change', function () { makeDirty(); - }) + }); } selectedLinks = null; @@ -706,19 +703,19 @@ $(function () { }); - $('#renameDevice').on('click touchstart', function () { - let targetName = $(this).parent().children('#content') + $('#renameDevice').on('click touch', function () { + let targetName = $(this).parent().children('#content'); let newDevName = prompt($('#langRenameDevice').text(), data['name']); - if(newDevName === null){ + if (newDevName === null) { return; } - $.post('/myhome/Device/'+data['id']+'/saveCoreSettings', {name : newDevName}).done(function(ret) { + $.post('/myhome/Device/' + data['id'] + '/saveCoreSettings', {name: newDevName}).done(function (ret) { if (handleError(ret)) { return; } data['name'] = newDevName; targetName.html(newDevName); - let chTitle = $('#device_'+data['id']); + let chTitle = $('#device_' + data['id']); chTitle.prop('title', newDevName); }); }); @@ -726,7 +723,7 @@ $(function () { // add new synonym entry to conf. List //TODO add to DB // check if is existing - /* $('.configListAdd').on('click touchstart', function () { + /* $('.configListAdd').on('click touch', function () { let $parent = $(this).parent(); let $inp = $parent.children('.configInput'); if ($inp.val() != '') { @@ -736,7 +733,7 @@ $(function () { $parent.children('.configListCurrent').append("
  • " + $inp.val() + "
  • "); $inp.val(''); - $('.configListRemove').on('click touchstart', function () { + $('.configListRemove').on('click touch', function () { $(this).parent().remove(); }); }); @@ -814,18 +811,18 @@ $(function () { } function newConfigListVal($parent, val, deletionLink) { - $parent.find('.configListCurrent').append("
  • " + val + "
  • "); - $('.configListRemove').on('click touchstart', function () { + $parent.find('.configListCurrent').append('
  • ' + val + '
  • '); + $('.configListRemove').on('click touch', function () { $(this).parent().remove(); - $.post(deletionLink, { 'value': val }) + $.post(deletionLink, {'value': val}); }); } // handle toolbar // save, hide toolbars, restore live view //unused - $('#saveToolbarAction').on('click touchstart', function () { - if($(sZone).hasClass('blueprint')) { + $('#saveToolbarAction').on('click touch', function () { + if ($(sZone).hasClass('blueprint')) { setBPMode(false); saveHouse(); setBPMode(true); @@ -834,7 +831,7 @@ $(function () { } }); - $('#finishToolbarAction').on('click touchstart', function () { + $('#finishToolbarAction').on('click touch', function () { setBPMode(false); saveHouse(); initEditable(); @@ -849,14 +846,14 @@ $(function () { }); // enter edit mode - $('#toolbarToggleShow').on('click touchstart', function () { + $('#toolbarToggleShow').on('click touch', function () { $('#toolbarOverview').show(); $('#toolbarToggle').hide(); initEditable(); }); // enter construction/location mode - $('#toolbarConstructionShow').on('click touchstart', function () { + $('#toolbarConstructionShow').on('click touch', function () { initEditable(); setBPMode(false); locationEditMode = true; @@ -865,7 +862,7 @@ $(function () { }); // enter device editing mode - $('#toolbarTechnicShow').on('click touchstart', function () { + $('#toolbarTechnicShow').on('click touch', function () { initEditable(); setBPMode(true); deviceEditMode = true; @@ -873,13 +870,13 @@ $(function () { $('#toolbarTechnic').show(); }); - $('#toolbarOverviewShow').on('click touchstart', function () { + $('#toolbarOverviewShow').on('click touch', function () { $('#toolbarOverview').show(); $('#toolbarToggle').hide(); initEditable(); }); - $floorPlan.on('click touchstart', function (e) { + $floorPlan.on('click touch', function (e) { if (!zoneMode) { return; } @@ -888,8 +885,8 @@ $(function () { let x = $(this).offset().left; let y = $(this).offset().top; - $.post('/myhome/Location/0/add', {name : zoneName}).done(function(data){ - if( handleError(data) ) { + $.post('/myhome/Location/0/add', {name: zoneName}).done(function (data) { + if (handleError(data)) { return; } let zoneId = data['id']; @@ -961,12 +958,12 @@ $(function () { } // construction tools - $('#addZone').on('click touchstart', function () { - if(!zoneMode) { + $('#addZone').on('click touch', function () { + if (!zoneMode) { markSelectedTool($(this)); zoneMode = true; $floorPlan.addClass(classAddZone); - removeResizableRotatableAndDraggable($(sZone+", "+sDeco+", "+sWall+", "+sConstr)); + removeResizableRotatableAndDraggable($(sZone + ', ' + sDeco + ', ' + sWall + ', ' + sConstr)); } else { zoneMode = false; markSelectedTool(null); @@ -974,7 +971,7 @@ $(function () { } }); - $('#builder').on('click touchstart', function () { + $('#builder').on('click touch', function () { if (!buildingMode) { markSelectedTool($(this)); @@ -983,7 +980,7 @@ $(function () { $('#constructionTiles').css('display', 'flex'); makeDroppable($(sZone), true); makeResizableRotatableAndDraggable($(sWallConstr)); - removeResizableRotatableAndDraggable($(sZone+", "+sDeco)); + removeResizableRotatableAndDraggable($(sZone + ', ' + sDeco)); } else { removeResizableRotatableAndDraggable($(sWallConstr)); $(sZone).droppable('destroy'); @@ -993,14 +990,14 @@ $(function () { } }); - $('#painter').on('click touchstart', function () { + $('#painter').on('click touch', function () { if (!paintingMode) { markSelectedTool($(this)); paintingMode = true; $('#painterTiles').css('display', 'flex'); $floorPlan.removeClass(classAddZone); - removeResizableRotatableAndDraggable($(sZone+", "+sDeco+", "+sWall+", "+sConstr)); + removeResizableRotatableAndDraggable($(sZone + ', ' + sDeco + ', ' + sWall + ', ' + sConstr)); } else { paintingMode = false; markSelectedTool(null); @@ -1008,14 +1005,14 @@ $(function () { } }); - $('#locationMover').on('click touchstart', function () { + $('#locationMover').on('click touch', function () { if (!locationMoveMode) { markSelectedTool($(this)); locationMoveMode = true; $('.zindexer').show(); makeResizableRotatableAndDraggable($(sZone)); - removeResizableRotatableAndDraggable($(sDeco+", "+sWallConstr)); + removeResizableRotatableAndDraggable($(sDeco + ', ' + sWallConstr)); makeDroppable($floorPlan, false); @@ -1026,7 +1023,7 @@ $(function () { } }); - $('#decorator').on('click touchstart', function () { + $('#decorator').on('click touch', function () { if (!decoratorMode) { markSelectedTool($(this)); @@ -1037,7 +1034,7 @@ $(function () { makeDroppable($(sZone), true); makeResizableRotatableAndDraggable($(sDeco)); - removeResizableRotatableAndDraggable($(sZone+", "+sWallConstr)); + removeResizableRotatableAndDraggable($(sZone + ', ' + sWallConstr)); } else { $(sZone).droppable('destroy'); removeResizableRotatableAndDraggable($(sDeco)); @@ -1045,13 +1042,13 @@ $(function () { } }); - $('#locationSettings').on('click touchstart', function () { + $('#locationSettings').on('click touch', function () { if (!locationSettingsMode) { markSelectedTool($(this)); locationSettingsMode = true; $(sDeco).css('pointer-events', 'none'); - removeResizableRotatableAndDraggable($(sZone+", "+sDeco+", "+sWall+", "+sConstr)); + removeResizableRotatableAndDraggable($(sZone + ', ' + sDeco + ', ' + sWall + ', ' + sConstr)); } else { locationSettingsMode = false; markSelectedTool(null); @@ -1059,7 +1056,7 @@ $(function () { }); // technic tools - $('#deviceInstaller').on('click touchstart', function () { + $('#deviceInstaller').on('click touch', function () { if (!deviceInstallerMode) { markSelectedTool($(this)); @@ -1074,13 +1071,13 @@ $(function () { } }); - $('#deviceLinker').on('click touchstart', function () { + $('#deviceLinker').on('click touch', function () { - if(!deviceLinkerMode){ + if (!deviceLinkerMode) { markSelectedTool($(this)); deviceLinkerMode = true; removeResizableRotatableAndDraggable($(sDevice)); - }else{ + } else { deviceLinkerMode = false; markSelectedTool(null); setSelectedDevice(false); @@ -1088,7 +1085,7 @@ $(function () { }); - $('#deviceMover').on('click touchstart', function () { + $('#deviceMover').on('click touch', function () { if (!deviceMoveMode) { markSelectedTool($(this)); @@ -1097,8 +1094,8 @@ $(function () { makeResizableRotatableAndDraggable($(sDevice)); $(sZone).droppable({ - drop: function( event, ui ) { - let userConf = false; + drop: function (event, ui) { + let userConf = false; let roomChange = false; let errorOccured = false; // check if room didn't change @@ -1141,7 +1138,7 @@ $(function () { } setTimeout( function() { ui.draggable.draggable( 'option', 'revert', true ); }, 1000 ); } - } + } }); } else { @@ -1151,7 +1148,7 @@ $(function () { } }); - $('#deviceSettings').on('click touchstart', function () { + $('#deviceSettings').on('click touch', function () { if (!deviceSettingsMode) { markSelectedTool($(this)); @@ -1177,7 +1174,7 @@ $(function () { for (let i = 1; i <= 11; i++) { // noinspection CssUnknownTarget let $tile = $('
    '); - $tile.on('click touchstart', function () { + $tile.on('click touch', function () { if (!$(this).hasClass('selected')) { $(sTile).removeClass('selected'); $(this).addClass('selected'); @@ -1193,7 +1190,7 @@ $(function () { // load floor tiles for (let i = 1; i <= 80; i++) { let $tile = $('
    '); - $tile.on('click touchstart', function () { + $tile.on('click touch', function () { if (!$(this).hasClass('selected')) { $(sTile).removeClass('selected'); $(this).addClass('selected'); @@ -1211,7 +1208,7 @@ $(function () { for (let i = 1; i <= 167; i++) { // noinspection CssUnknownTarget let $tile = $('
    '); - $tile.on('click touchstart', function () { + $tile.on('click touch', function () { if (!$(this).hasClass('selected')) { $(sTile).removeClass('selected'); $(this).addClass('selected'); @@ -1228,7 +1225,7 @@ $(function () { $.each(dats, function(k, dat) { // noinspection CssUnknownTarget let $tile = $('
    '); - $tile.on('click touchstart', function () { + $tile.on('click touch', function () { if (!$(this).hasClass('selected')) { $(sTile).removeClass('selected'); $(this).addClass('selected'); diff --git a/core/interface/static/js/skills.js b/core/interface/static/js/skills.js index 8505f9fdc..ff727b0a4 100644 --- a/core/interface/static/js/skills.js +++ b/core/interface/static/js/skills.js @@ -71,7 +71,7 @@ $(function () { let $actions = $('
    '); let $button = $('
    '); - $button.on('click touchstart', function () { + $button.on('click touch', function () { $(this).hide(); $(this).parent().children('.skillStoreSkillDownloadButton').css('display', 'flex'); for (let i = 0; i < selectedSkillsToDownload.length; i++) { @@ -88,7 +88,7 @@ $(function () { $actions.append($button); $button = $('
    '); - $button.on('click touchstart', function () { + $button.on('click touch', function () { $(this).hide(); $(this).parent().children('.skillStoreSkillSelected').css('display', 'flex'); selectedSkillsToDownload.push({'skill': installer['name'], 'author': installer['author']}); @@ -111,17 +111,17 @@ $(function () { function loadStoreData() { $.ajax({ - url: '/skills/loadStoreData/', + url : '/skills/loadStoreData/', type: 'POST' - }).done(function (answer){ + }).done(function (answer) { $('#skillStoreWait').hide(); - $.each(answer, function(skillName, installer){ + $.each(answer, function (skillName, installer) { addToStore(installer); }); }); } - $applySkillStore.on('click touchstart', function () { + $applySkillStore.on('click touch', function () { $('.skillStoreSkillSelected').hide(); $(this).hide(); $.each(selectedSkillsToDownload, function (index, skill) { @@ -129,8 +129,8 @@ $(function () { }); $.ajax({ - url: '/skills/installSkills/', - data: JSON.stringify(selectedSkillsToDownload), + url : '/skills/installSkills/', + data : JSON.stringify(selectedSkillsToDownload), contentType: 'application/json', dataType: 'json', type: 'POST' @@ -145,9 +145,9 @@ $(function () { return false; }); - $('[id^=toggle_]').on('click touchstart', function () { + $('[id^=toggle_]').on('click touch', function () { $.ajax({ - url: '/skills/toggleSkill/', + url : '/skills/toggleSkill/', data: { id: $(this).attr('id') }, @@ -168,18 +168,18 @@ $(function () { }); $('[id^=instructions_for_]').dialog({ - autoOpen: false, + autoOpen : false, draggable: false, - width: '60%', - height: 600, - modal: true, + width : '60%', + height : 600, + modal : true, resizable: false }); - $('[id^=update_]').on('click touchstart', function () { + $('[id^=update_]').on('click touch', function () { let $self = $(this); $.ajax({ - url: '/skills/updateSkill/', + url : '/skills/updateSkill/', data: { id: $(this).attr('id') }, @@ -194,31 +194,31 @@ $(function () { return false; }); - $('.skillSettings').on('click touchstart', function () { + $('.skillSettings').on('click touch', function () { $('#config_for_' + $(this).attr('data-forSkill')).dialog('open'); return false; }); - $('.skillViewIntents').on('click touchstart', function () { + $('.skillViewIntents').on('click touch', function () { $(this).parent('.skillDefaultView').css('display', 'none'); $(this).parent().parent().children('.skillIntentsView').css('display', 'flex'); return false; }); - $('.skillIntentsViewCloseButton').on('click touchstart', function () { + $('.skillIntentsViewCloseButton').on('click touch', function () { $(this).parent().parent().children('.skillDefaultView').css('display', 'flex'); $(this).parent('.skillIntentsView').css('display', 'none'); return false; }); - $('.skillInstructions').on('click touchstart', function () { + $('.skillInstructions').on('click touch', function () { $('#instructions_for_' + $(this).attr('data-forSkill')).dialog('open'); return false; }); - $('[id^=delete_]').on('click touchstart', function () { + $('[id^=delete_]').on('click touch', function () { $.ajax({ - url: '/skills/deleteSkill/', + url : '/skills/deleteSkill/', data: { id: $(this).attr('id') }, @@ -229,9 +229,9 @@ $(function () { return false; }); - $('[id^=reload_]').on('click touchstart', function () { + $('[id^=reload_]').on('click touch', function () { $.ajax({ - url: '/skills/reloadSkill/', + url : '/skills/reloadSkill/', data: { id: $(this).attr('id') }, @@ -242,7 +242,7 @@ $(function () { return false; }); - $('#openSkillStore').on('click touchstart', function () { + $('#openSkillStore').on('click touch', function () { loadStoreData(); $('#skillsPane').hide(); $('#skillStore').css('display', 'flex'); @@ -251,7 +251,7 @@ $(function () { return false; }); - $('#closeSkillStore').on('click touchstart', function () { + $('#closeSkillStore').on('click touch', function () { location.reload(); return false; }); diff --git a/core/interface/static/js/syslog.js b/core/interface/static/js/syslog.js index 9b0abb84c..a8feb0ee9 100644 --- a/core/interface/static/js/syslog.js +++ b/core/interface/static/js/syslog.js @@ -23,13 +23,13 @@ $(function () { } } - $stopScroll.on('click touchstart', function () { + $stopScroll.on('click touch', function () { $(this).hide(); $startScroll.show(); return false; }); - $startScroll.on('click touchstart', function () { + $startScroll.on('click touch', function () { $(this).hide(); $stopScroll.show(); return false; @@ -39,7 +39,7 @@ $(function () { MQTT.subscribe('projectalice/logging/syslog'); } - for(let i = 0; i < logHistory.length; i++) { + for (let i = 0; i < logHistory.length; i++) { addToLogs(logHistory[i]); } diff --git a/core/interface/static/js/widgets.js b/core/interface/static/js/widgets.js index 380040b48..e651f61f0 100644 --- a/core/interface/static/js/widgets.js +++ b/core/interface/static/js/widgets.js @@ -13,22 +13,18 @@ $(function () { disabled: true }); - $widgets.each(function () { - initIndexers($(this)); - }); - /* Toolbar Functions */ - $('#toolbarToggleShow').on('click touchstart', function () { + $('#toolbarToggleShow').on('click touch', function () { $('#toolbar_full').show(); $('#toolbar_toggle').hide(); let widget = $('.widget'); - widget.css('outline', "2px var(--accent) dotted"); + widget.css('outline', '2px var(--accent) dotted'); widget.draggable('enable'); widget.resizable('enable'); $('.zindexer').show(); }); - $('#toolbarToggleHide').on('click touchstart', function () { + $('#toolbarToggleHide').on('click touch', function () { $('#toolbar_full').hide(); $('#toolbar_toggle').show(); let widget = $('.widget'); @@ -36,7 +32,11 @@ $(function () { widget.draggable('disable'); widget.resizable('disable'); $('.zindexer').hide(); + saveWidgets(); + }); + function saveWidgets() { + let widget = $('.widget'); let data = {}; widget.each(function () { data[$(this).attr('id')] = { @@ -46,7 +46,7 @@ $(function () { w : $(this).outerWidth(), h : $(this).outerHeight(), zindex: $(this).css('z-index') - } + }; }); $.ajax({ @@ -56,7 +56,7 @@ $(function () { dataType : 'json', type : 'POST' }); - }); + } $('.widgetOptions').dialog({ autoOpen : false, @@ -85,26 +85,29 @@ $(function () { }); - $('#removeWidget').on('click touchstart', function () { + $('#removeWidget').on('click touch', function () { $('.widgetDelete').show(); + $('.widgetConfig').hide(); + $('.zindexer').hide(); $('#toolbar_checkmark').show(); $('#toolbar_full').hide(); return false; }); - $('#addWidget').on('click touchstart', function () { + $('#addWidget').on('click touch', function () { $('#addWidgetDialog').dialog('open'); return false; }); - $('#configToggle').on('click touchstart', function () { + $('#configToggle').on('click touch', function () { $('.widgetConfig').show(); + $('.zindexer').hide(); $('#toolbar_checkmark').show(); $('#toolbar_full').hide(); return false; }); - $('#cinemaToggle').on('click touchstart', function () { + $('#cinemaToggle').on('click touch', function () { $('nav').toggle(); $('#toolbar_full').hide(); $('header').toggle(); @@ -115,17 +118,20 @@ $(function () { $('.zindexer').hide(); }); - $('#widgetCheck').on('click touchstart', function () { + $('#widgetCheck').on('click touch', function () { $('#toolbar_checkmark').hide(); $('.widgetDelete').hide(); $('.widgetConfig').hide(); + + saveWidgets(); + location.reload(); return false; }); /*=================== Functions for the single widgets ======================*/ /* Remove the selected widget */ - $('.widgetDelete').on('click touchstart', function () { + $('.widgetDelete').on('click touch', function () { if ($(this).parents('.widget').length > 0) { $.post('/home/removeWidget/', {id: $(this).parent().attr('id')}); $(this).parent().remove(); @@ -145,8 +151,8 @@ $(function () { } // build configuration - let newForm = "
    "; - newForm += ""; + let newForm = ''; + newForm += ''; jQuery.each(data, function (i, val) { let input = '
    '; if (i == 'background') { @@ -170,9 +176,9 @@ $(function () { input += '
    '; } - newForm += "
    " + input; + newForm += '
    ' + input; }); - newForm += "
    "; + newForm += '
    '; dialogContainer.find('#' + tab).html(newForm); // perform submit/save of the form without switching page @@ -208,7 +214,7 @@ $(function () { } /* Opening of widget specific settings */ - $('.widgetConfig').on('click touchstart', function () { + $('.widgetConfig').on('click touch', function () { if ($(this).parents('.widget').length > 0) { let parent = $(this).parent(); prepareConfigTab(parent, 'WidgetConfig'); @@ -217,7 +223,7 @@ $(function () { return false; }); - $('.addWidgetCheck').on('click touchstart', function () { + $('.addWidgetCheck').on('click touch', function () { if ($(this).parents('.addWidgetLine').length > 0) { $.post('/home/addWidget/', {id: $(this).parent().attr('id')}); $(this).parent().remove(); diff --git a/core/interface/templates/base.html b/core/interface/templates/base.html index 973e30332..86fa628ba 100644 --- a/core/interface/templates/base.html +++ b/core/interface/templates/base.html @@ -60,7 +60,9 @@ - + {% if aliceSettings['scenariosActive'] is sameas true %} + + {% endif %} diff --git a/core/interface/templates/devmode.html b/core/interface/templates/devmode.html index ca6163657..ec36d2bc8 100644 --- a/core/interface/templates/devmode.html +++ b/core/interface/templates/devmode.html @@ -29,6 +29,14 @@
    +
    + +
    + + + +
    +
    @@ -37,6 +45,27 @@
    +
    + +
    + +
    +
    @@ -47,7 +76,15 @@
    - +
    + + +
    +
    +
    + +
    +
    @@ -84,8 +121,13 @@
    - + +
    +
    +
    + +
    +
    @@ -103,7 +145,8 @@ {% for skillName, skill in skills.items() %}
    -
    {{ skillName }}
    +
    {{ skillName }}
    +
    {% endfor %} diff --git a/core/interface/templates/home.html b/core/interface/templates/home.html index 822204f53..c31db0266 100644 --- a/core/interface/templates/home.html +++ b/core/interface/templates/home.html @@ -62,7 +62,8 @@ {% for widgetName, widget in widgetList.items() %}
    {{ widgetName }}
    - +
    {% endfor %}
    @@ -73,33 +74,33 @@ {% for parentName, widgetList in widgets.items() %} {% for widgetName, widget in widgetList.items() %} {% if (widget.state|int) == 1 %} -
    {{ widget.html()|safe }}
    @@ -108,10 +109,6 @@
    -
    -
    -
    -
    {% endif %} {% endfor %} diff --git a/core/interface/templates/scenarios.html b/core/interface/templates/scenarios.html index 032f30292..9563027fb 100644 --- a/core/interface/templates/scenarios.html +++ b/core/interface/templates/scenarios.html @@ -7,10 +7,6 @@ {% block loaders %} - - - - {% endblock %} {% block pageTitle %} @@ -18,5 +14,5 @@ {% endblock %} {% block content %} - + {% endblock %} diff --git a/core/interface/views/AdminView.py b/core/interface/views/AdminView.py index f95a27aa5..75c89e006 100644 --- a/core/interface/views/AdminView.py +++ b/core/interface/views/AdminView.py @@ -3,6 +3,7 @@ from flask import jsonify, render_template, request from flask_login import login_required +from core.ProjectAliceExceptions import ConfigurationUpdateFailed from core.interface.model.View import View @@ -49,23 +50,16 @@ def saveAliceSettings(self): # or float because HTTP data is type less. confs = {key: self.retrieveValue(value) for key, value in request.form.items()} - postProcessing = set() for conf, value in confs.items(): if value == self.ConfigManager.getAliceConfigByName(conf): continue - pre = self.ConfigManager.getAliceConfUpdatePreProcessing(conf) - if pre and not self.ConfigManager.doConfigUpdatePreProcessing(pre, value): - continue - - pp = self.ConfigManager.getAliceConfUpdatePostProcessing(conf) - if pp: - postProcessing.add(pp) - - confs['supportedLanguages'] = self.ConfigManager.getAliceConfigByName('supportedLanguages') + try: + self.ConfigManager.updateAliceConfiguration(conf, value, False) + except ConfigurationUpdateFailed as e: + self.logError(f'Updating config failed for **{conf}**: {e}') - self.ConfigManager.writeToAliceConfigurationFile(confs=confs) - self.ConfigManager.doConfigUpdatePostProcessing(postProcessing) + self.ConfigManager.writeToAliceConfigurationFile() return self.index() except Exception as e: self.logError(f'Failed saving Alice config: {e}') diff --git a/core/interface/views/DevModeView.py b/core/interface/views/DevModeView.py index 9e728477f..9fb9984ff 100644 --- a/core/interface/views/DevModeView.py +++ b/core/interface/views/DevModeView.py @@ -44,21 +44,27 @@ def put(self, skillName: str): try: newSkill = { 'name' : skillName, + 'speakableName' : request.form.get('speakableName', ''), 'description' : request.form.get('description', 'Missing description'), + 'category' : request.form.get('skillCategory', 'undefined'), 'fr' : request.form.get('fr', False), 'de' : request.form.get('de', False), 'it' : request.form.get('it', False), - 'pipreq' : request.form.get('pipreq', list()), - 'sysreq' : request.form.get('sysreq', list()), + 'pl' : request.form.get('pl', False), + 'instructions' : request.form.get('createInstructions', False), + 'pipreq' : request.form.get('pipreq', ''), + 'sysreq' : request.form.get('sysreq', ''), 'conditionOnline' : request.form.get('conditionOnline', False), 'conditionASRArbitrary' : request.form.get('conditionASRArbitrary', False), - 'conditionSkill' : request.form.get('conditionSkill', list()), - 'conditionNotSkill' : request.form.get('conditionNotSkill', list()), - 'conditionActiveManager': request.form.get('conditionActiveManager', list()), - 'widgets' : request.form.get('widgets', list()) + 'conditionSkill' : request.form.get('conditionSkill', ''), + 'conditionNotSkill' : request.form.get('conditionNotSkill', ''), + 'conditionActiveManager': request.form.get('conditionActiveManager', ''), + 'widgets' : request.form.get('widgets', ''), + 'nodes' : request.form.get('nodes', '') } + if not self.SkillManager.createNewSkill(newSkill): - raise Exception('Unhandled skill creation exception') + raise Exception return jsonify(success=True) diff --git a/core/interface/views/IndexView.py b/core/interface/views/IndexView.py index 98431190a..d59f638bb 100644 --- a/core/interface/views/IndexView.py +++ b/core/interface/views/IndexView.py @@ -54,7 +54,7 @@ def removeWidget(self): widget = self.SkillManager.widgets[parent][widgetName] widget.state = 0 - widget.saveToDB() + widget.zindex = -1 self.SkillManager.sortWidgetZIndexes() return jsonify(success=True) @@ -70,7 +70,7 @@ def addWidget(self): widget = self.SkillManager.widgets[parent][widgetName] widget.state = 1 - widget.saveToDB() + widget.zindex = self.SkillManager.nextZIndex() self.SkillManager.sortWidgetZIndexes() return redirect('home.html') diff --git a/core/nlu/NluManager.py b/core/nlu/NluManager.py index b6c660f5b..20b82fc19 100644 --- a/core/nlu/NluManager.py +++ b/core/nlu/NluManager.py @@ -12,6 +12,7 @@ def __init__(self): self._pathToCache = Path(self.Commons.rootDir(), 'var/cache/nlu/trainingData') if not self._pathToCache.exists(): self._pathToCache.mkdir(parents=True) + self._training = False def onStart(self): @@ -72,3 +73,13 @@ def trainNLU(self): def clearCache(self): shutil.rmtree(self._pathToCache) self._pathToCache.mkdir() + + + @property + def training(self) -> bool: + return self._training + + + @training.setter + def training(self, value: bool): + self._training = value diff --git a/core/nlu/model/SnipsNlu.py b/core/nlu/model/SnipsNlu.py index a57591a72..4dbf4f0d5 100644 --- a/core/nlu/model/SnipsNlu.py +++ b/core/nlu/model/SnipsNlu.py @@ -1,9 +1,8 @@ import json -from pathlib import Path -from subprocess import CompletedProcess - import re import shutil +from pathlib import Path +from subprocess import CompletedProcess from core.commons import constants from core.nlu.model.NluEngine import NluEngine @@ -98,69 +97,82 @@ def convertDialogTemplate(self, file: Path): def train(self): + if self.NluManager.training: + self.logWarning("NLU is already training, can't train again now") + return + self.logInfo('Training Snips NLU') - dataset = { - 'entities': dict(), - 'intents' : dict(), - 'language': self.LanguageManager.activeLanguage, - } + try: + self.NluManager.training = True + dataset = { + 'entities': dict(), + 'intents' : dict(), + 'language': self.LanguageManager.activeLanguage, + } - with Path(self._cachePath / f'{self.LanguageManager.activeLanguage}.json').open() as fp: - trainingData = json.load(fp) - dataset['entities'].update(trainingData['entities']) - dataset['intents'].update(trainingData['intents']) + with Path(self._cachePath / f'{self.LanguageManager.activeLanguage}.json').open() as fp: + trainingData = json.load(fp) + dataset['entities'].update(trainingData['entities']) + dataset['intents'].update(trainingData['intents']) - datasetFile = Path('/tmp/snipsNluDataset.json') + datasetFile = Path('/tmp/snipsNluDataset.json') - with datasetFile.open('w') as fp: - json.dump(dataset, fp, ensure_ascii=False, indent=4) + with datasetFile.open('w') as fp: + json.dump(dataset, fp, ensure_ascii=False, indent='\t') - self.logInfo('Generated dataset for training') + self.logInfo('Generated dataset for training') - # Now that we have generated the dataset, let's train in the background if we are already booted, else do it directly - if self.ProjectAlice.isBooted: - self.ThreadManager.newThread(name='NLUTraining', target=self.nluTrainingThread, args=[datasetFile]) - else: - self.nluTrainingThread(datasetFile) + # Now that we have generated the dataset, let's train in the background if we are already booted, else do it directly + if self.ProjectAlice.isBooted: + self.ThreadManager.newThread(name='NLUTraining', target=self.nluTrainingThread, args=[datasetFile]) + else: + self.nluTrainingThread(datasetFile) + except: + self.NluManager.training = False def nluTrainingThread(self, datasetFile: Path): - with Stopwatch() as stopWatch: - self.logInfo('Begin training...') - self._timer = self.ThreadManager.newTimer(interval=0.25, func=self.trainingStatus) + try: + with Stopwatch() as stopWatch: + self.logInfo('Begin training...') + self._timer = self.ThreadManager.newTimer(interval=0.25, func=self.trainingStatus) - tempTrainingData = Path('/tmp/snipsNLU') + tempTrainingData = Path('/tmp/snipsNLU') - if tempTrainingData.exists(): - shutil.rmtree(tempTrainingData) + if tempTrainingData.exists(): + shutil.rmtree(tempTrainingData) - training: CompletedProcess = self.Commons.runSystemCommand([f'./venv/bin/snips-nlu', 'train', str(datasetFile), str(tempTrainingData)]) - if training.returncode != 0: - self.logError(f'Error while training Snips NLU: {training.stderr.decode()}') + training: CompletedProcess = self.Commons.runSystemCommand([f'./venv/bin/snips-nlu', 'train', str(datasetFile), str(tempTrainingData)]) + if training.returncode != 0: + self.logError(f'Error while training Snips NLU: {training.stderr.decode()}') - assistantPath = Path(self.Commons.rootDir(), f'trained/assistants/{self.LanguageManager.activeLanguage}/nlu_engine') + assistantPath = Path(self.Commons.rootDir(), f'trained/assistants/{self.LanguageManager.activeLanguage}/nlu_engine') - if not tempTrainingData.exists(): - self.logError('Snips NLU training failed') - self.MqttManager.publish(constants.TOPIC_NLU_TRAINING_STATUS, payload={'status': 'failed'}) - if not assistantPath.exists(): - self.logFatal('No NLU engine found, cannot start') + if not tempTrainingData.exists(): + self.logError('Snips NLU training failed') + self.MqttManager.publish(constants.TOPIC_NLU_TRAINING_STATUS, payload={'status': 'failed'}) + if not assistantPath.exists(): + self.logFatal('No NLU engine found, cannot start') - self._timer.cancel() - return + self._timer.cancel() + return - if assistantPath.exists(): - shutil.rmtree(assistantPath) + if assistantPath.exists(): + shutil.rmtree(assistantPath) - shutil.move(tempTrainingData, assistantPath) + shutil.move(tempTrainingData, assistantPath) - self.broadcast(method=constants.EVENT_NLU_TRAINED, exceptions=[constants.DUMMY], propagateToSkills=True) - self.Commons.runRootSystemCommand(['systemctl', 'restart', 'snips-nlu']) + self.broadcast(method=constants.EVENT_NLU_TRAINED, exceptions=[constants.DUMMY], propagateToSkills=True) + self.Commons.runRootSystemCommand(['systemctl', 'restart', 'snips-nlu']) - self._timer.cancel() - self.MqttManager.publish(constants.TOPIC_NLU_TRAINING_STATUS, payload={'status': 'done'}) - self.ThreadManager.getEvent('TrainAssistant').clear() - self.logInfo(f'Snips NLU trained in {stopWatch} seconds') + self._timer.cancel() + self.MqttManager.publish(constants.TOPIC_NLU_TRAINING_STATUS, payload={'status': 'done'}) + self.ThreadManager.getEvent('TrainAssistant').clear() + self.logInfo(f'Snips NLU trained in {stopWatch} seconds') + except: + pass # Do nothing, the finally clause will finish the work + finally: + self.NluManager.training = False def trainingStatus(self): diff --git a/core/scenario/__init__.py b/core/scenario/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/scenario/model/ScenarioTile.py b/core/scenario/model/ScenarioTile.py deleted file mode 100644 index f9c78e972..000000000 --- a/core/scenario/model/ScenarioTile.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Any - -from core.base.model.ProjectAliceObject import ProjectAliceObject -from core.scenario.model.ScenarioTileType import ScenarioTileType - - -class ScenarioTile(ProjectAliceObject): - - def __init__(self): - super().__init__() - self.tileType: ScenarioTileType = ScenarioTileType.ACTION - self.name: str = '' - self.description: str = '' - self.value: Any = '' - - - def interfaceName(self) -> str: - return self.name diff --git a/core/scenario/model/ScenarioTileType.py b/core/scenario/model/ScenarioTileType.py deleted file mode 100644 index cc4f50b00..000000000 --- a/core/scenario/model/ScenarioTileType.py +++ /dev/null @@ -1,14 +0,0 @@ -from enum import Enum - - -class ScenarioTileType(Enum): - CONDITION_BLOCK = 10 - LOOP_BLOCK = 20 - VARIABLE = 30 - INTEGER = 40 - STRING = 50 - FLOAT = 60 - BOOLEAN = 70 - ARRAY = 80 - ACTION = 150 - EVENT = 200 diff --git a/core/scenario/model/__init__.py b/core/scenario/model/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/server/AudioServer.py b/core/server/AudioServer.py index 7b17a6680..1f56d5b5a 100644 --- a/core/server/AudioServer.py +++ b/core/server/AudioServer.py @@ -4,7 +4,8 @@ from pathlib import Path from typing import Dict, Optional -import pyaudio +import sounddevice as sd +# noinspection PyUnresolvedReferences from webrtcvad import Vad from core.ProjectAliceExceptions import PlayBytesStopped @@ -22,41 +23,49 @@ class AudioManager(Manager): LAST_USER_SPEECH = 'var/cache/lastUserpeech_{}_{}.wav' SECOND_LAST_USER_SPEECH = 'var/cache/secondLastUserSpeech_{}_{}.wav' - # Inspired by https://github.com/koenvervloesem/hermes-audio-server - def __init__(self): super().__init__() self._stopPlayingFlag: Optional[AliceEvent] = None self._playing = False self._waves: Dict[str, wave.Wave_write] = dict() + self._audioInputStream = None if self.ConfigManager.getAliceConfigByName('disableSoundAndMic'): return - with self.Commons.shutUpAlsaFFS(): - self._audio = pyaudio.PyAudio() - self._vad = Vad(2) + self._audioInput = None + self._audioOutput = None - try: - self._audioOutput = self._audio.get_default_output_device_info() - except: - self.logFatal('Audio output not found, cannot continue') - return + + def onStart(self): + super().onStart() + + if not self.ConfigManager.getAliceConfigByName('inputDevice'): + self.logWarning('Input device not set in config, trying to find default device') + try: + self._audioInput = sd.query_devices(kind='input')['name'] + except: + self.logFatal('Audio input not found, cannot continue') + return + self.ConfigManager.updateAliceConfiguration(key='inputDevice', value=self._audioInput) else: - self.logInfo(f'Using **{self._audioOutput["name"]}** for audio output') + self._audioInput = self.ConfigManager.getAliceConfigByName('inputDevice') - try: - self._audioInput = self._audio.get_default_input_device_info() - except: - self.logFatal('Audio input not found, cannot continue') + if not self.ConfigManager.getAliceConfigByName('outputDevice'): + self.logWarning('Output device not set in config, trying to find default device') + try: + self._audioOutput = sd.query_devices(kind='output')['name'] + except: + self.logFatal('Audio output not found, cannot continue') + return + self.ConfigManager.updateAliceConfiguration(key='outputDevice', value=self._audioOutput) else: - self.logInfo(f'Using **{self._audioInput["name"]}** for audio input') + self._audioOutput = self.ConfigManager.getAliceConfigByName('outputDevice') + self.setDefaults() - def onStart(self): - super().onStart() self._stopPlayingFlag = self.ThreadManager.newEvent('stopPlaying') self.MqttManager.mqttClient.subscribe(constants.TOPIC_AUDIO_FRAME.format(self.ConfigManager.getAliceConfigByName('uuid'))) @@ -64,13 +73,19 @@ def onStart(self): self.ThreadManager.newThread(name='audioPublisher', target=self.publishAudio) + def setDefaults(self): + self.logInfo(f'Using **{self._audioInput}** for audio input') + self.logInfo(f'Using **{self._audioOutput}** for audio output') + + sd.default.device = self._audioInput, self._audioOutput + + def onStop(self): super().onStop() + self._audioInputStream.stop(ignore_errors=True) + self._audioInputStream.close(ignore_errors=True) self.MqttManager.mqttClient.unsubscribe(constants.TOPIC_AUDIO_FRAME.format(self.ConfigManager.getAliceConfigByName('uuid'))) - if not self.ConfigManager.getAliceConfigByName('disableSoundAndMic'): - self._audio.terminate() - def onStartListening(self, session: DialogSession): if not self.ConfigManager.getAliceConfigByName('recordAudioAfterWakeword'): @@ -104,13 +119,13 @@ def recordFrame(self, siteId: str, frame: bytes): def publishAudio(self): self.logInfo('Starting audio publisher') - audioStream = self._audio.open( - format=pyaudio.paInt16, + self._audioInputStream = sd.RawInputStream( + dtype='int16', channels=1, - rate=self.SAMPLERATE, - frames_per_buffer=self.FRAMES_PER_BUFFER, - input=True + samplerate=self.SAMPLERATE, + blocksize=self.FRAMES_PER_BUFFER, ) + self._audioInputStream.start() speech = False silence = self.SAMPLERATE / self.FRAMES_PER_BUFFER @@ -122,7 +137,7 @@ def publishAudio(self): break try: - frames = audioStream.read(num_frames=self.FRAMES_PER_BUFFER, exception_on_overflow=False) + frames = self._audioInputStream.read(frames=self.FRAMES_PER_BUFFER)[0] if self._vad.is_speech(frames, self.SAMPLERATE): if not speech and speechFrames < minSpeechFrames: @@ -175,30 +190,31 @@ def onPlayBytes(self, requestId: str, payload: bytearray, siteId: str, sessionId with io.BytesIO(payload) as buffer: try: with wave.open(buffer, 'rb') as wav: - sampleWidth = wav.getsampwidth() - nFormat = self._audio.get_format_from_width(sampleWidth) channels = wav.getnchannels() framerate = wav.getframerate() - def streamCallback(_inData, frameCount, _timeInfo, _status) -> tuple: + def streamCallback(outdata, frameCount, _timeInfo, _status): data = wav.readframes(frameCount) - return data, pyaudio.paContinue + if len(data) < len(outdata): + outdata[:len(data)] = data + outdata[len(data):] = b'\x00' * (len(outdata) - len(data)) + raise sd.CallbackStop + else: + outdata[:] = data - audioStream = self._audio.open( - format=nFormat, + stream = sd.RawOutputStream( + dtype='int16', channels=channels, - rate=framerate, - output=True, - output_device_index=self._audioOutput['index'], - stream_callback=streamCallback + samplerate=framerate, + callback=streamCallback ) - self.logDebug(f'Playing wav stream using **{self._audioOutput["name"]}** audio output from site id **{self.DeviceManager.siteIdToDeviceName(siteId)}** (Format: {nFormat}, channels: {channels}, rate: {framerate})') - audioStream.start_stream() - while audioStream.is_active(): + self.logDebug(f'Playing wav stream using **{self._audioOutput}** audio output from site id **{self.DeviceManager.siteIdToDeviceName(siteId)}** (channels: {channels}, rate: {framerate})') + stream.start() + while stream.active: if self._stopPlayingFlag.is_set(): - audioStream.stop_stream() - audioStream.close() + stream.stop() + stream.close() if sessionId: self.MqttManager.publish( @@ -214,8 +230,8 @@ def streamCallback(_inData, frameCount, _timeInfo, _status) -> tuple: raise PlayBytesStopped time.sleep(0.1) - audioStream.stop_stream() - audioStream.close() + stream.stop() + stream.close() except PlayBytesStopped: self.logDebug('Playing bytes stopped') except Exception as e: @@ -228,7 +244,7 @@ def streamCallback(_inData, frameCount, _timeInfo, _status) -> tuple: self.MqttManager.publish( topic=constants.TOPIC_PLAY_BYTES_FINISHED.format(siteId), payload={ - 'id': requestId, + 'id' : requestId, 'sessionId': sessionId } ) @@ -238,6 +254,12 @@ def stopPlaying(self): self._stopPlayingFlag.set() + def updateAudioDevices(self): + self._audioInput = self.ConfigManager.getAliceConfigByName('inputDevice') + self._audioOutput = self.ConfigManager.getAliceConfigByName('outputDevice') + self.setDefaults() + + @property def isPlaying(self) -> bool: return self._playing diff --git a/core/server/MqttManager.py b/core/server/MqttManager.py index de021482e..b87b82a82 100644 --- a/core/server/MqttManager.py +++ b/core/server/MqttManager.py @@ -488,11 +488,13 @@ def intentNotRecognized(self, _client, _data, msg: mqtt.MQTTMessage): if session.notUnderstood <= self.ConfigManager.getAliceConfigByName('notUnderstoodRetries'): session.notUnderstood = session.notUnderstood + 1 - self.continueDialog( - sessionId=sessionId, - text=self.TalkManager.randomTalk('notUnderstood', skill='system'), - intentFilter=session.intentFilter - ) + + if not self.ConfigManager.getAliceConfigByName('suggestSkillsToInstall'): + self.continueDialog( + sessionId=sessionId, + text=self.TalkManager.randomTalk('notUnderstood', skill='system'), + intentFilter=session.intentFilter + ) else: session.notUnderstood = 0 self.endDialog(sessionId=sessionId, text=self.TalkManager.randomTalk('notUnderstoodEnd', skill='system')) diff --git a/core/util/Decorators.py b/core/util/Decorators.py index 6a6120b0c..6832cf313 100644 --- a/core/util/Decorators.py +++ b/core/util/Decorators.py @@ -2,7 +2,7 @@ import functools import warnings -from typing import Any, Callable, Tuple, Union +from typing import Any, Callable, Optional, Tuple, Union from flask import jsonify, request @@ -172,7 +172,7 @@ def wrapper(*args, **kwargs): return wrapper -def IfSetting(func: Callable = None, settingName: str = None, settingValue: Any = None, inverted: bool = False, skillName: str = None): #NOSONAR +def IfSetting(func: Callable = None, settingName: str = None, settingValue: Any = None, inverted: bool = False, skillName: str = None, returnValue: Optional[Any] = None): #NOSONAR """ Checks wheter a setting is equal to the given value before executing the wrapped method If the setting is not equal to the given value, the wrapped method is not called @@ -183,6 +183,7 @@ def IfSetting(func: Callable = None, settingName: str = None, settingValue: Any :param settingValue: :param inverted: :param skillName: + :param returnValue: The value to return if the setting check fails :return: """ @@ -199,11 +200,13 @@ def settingDecorator(*args, **kwargs): value = configManager.getSkillConfigByName(skillName, settingName) if skillName else configManager.getAliceConfigByName(settingName) if value is None: - return None + return returnValue if (not inverted and value == settingValue) or \ (inverted and value != settingValue): return func(*args, **kwargs) + else: + return returnValue return settingDecorator diff --git a/core/util/InternetManager.py b/core/util/InternetManager.py index 40de5ce14..31201596d 100644 --- a/core/util/InternetManager.py +++ b/core/util/InternetManager.py @@ -1,5 +1,4 @@ import requests -from requests.exceptions import RequestException from core.base.model.Manager import Manager from core.commons import constants @@ -10,12 +9,19 @@ class InternetManager(Manager): def __init__(self): super().__init__() self._online = False + self._checkThread = None + self._checkFrequency = 2 def onStart(self): super().onStart() if not self.ConfigManager.getAliceConfigByName('stayCompletlyOffline'): self.checkOnlineState(silent=True) + # 20 seconds is the max, 2 seconds the minimum + # We have 10 positions in the config (from 1 to 10) So the frequency = max / 10 * setting = 2 * setting + internetQuality = self.ConfigManager.getAliceConfigByName('internetQuality') or 1 + self._checkFrequency = internetQuality * 2 + self._checkThread = self.ThreadManager.newThread(name='internetCheckThread', target=self.checkInternet) else: self.logInfo('Configurations set to stay completly offline') @@ -29,15 +35,18 @@ def onBooted(self): self.checkOnlineState() - def onFullMinute(self): - if not self.ConfigManager.getAliceConfigByName('stayCompletlyOffline'): - self.checkOnlineState() + def checkInternet(self): + self.checkOnlineState() + self.ThreadManager.doLater(interval=self._checkFrequency, func=self.checkInternet) def checkOnlineState(self, addr: str = 'https://clients3.google.com/generate_204', silent: bool = False) -> bool: + if self.ConfigManager.getAliceConfigByName('stayCompletlyOffline'): + return False + try: online = requests.get(addr).status_code == 204 - except RequestException: + except: online = False if silent: diff --git a/core/voice/TTSManager.py b/core/voice/TTSManager.py index ad807afa4..bfb1ba99c 100644 --- a/core/voice/TTSManager.py +++ b/core/voice/TTSManager.py @@ -118,6 +118,11 @@ def onSay(self, session: DialogSession): if session.textOnly: return + if 'text' not in session.payload: + self.logWarning('Was asked to say something but no text provided') + self.MqttManager.endSession(sessionId=session.sessionId) + return + if session and session.user != constants.UNKNOWN_USER: user: User = self.UserManager.getUser(session.user) if user and user.tts: diff --git a/core/voice/WakewordRecorder.py b/core/voice/WakewordRecorder.py index 16adcdefe..be2ded6c4 100644 --- a/core/voice/WakewordRecorder.py +++ b/core/voice/WakewordRecorder.py @@ -233,7 +233,7 @@ def finalizeWakeword(self): path.mkdir() - (path/'config.json').write_text(json.dumps(config, indent=4)) + (path/'config.json').write_text(json.dumps(config, indent='\t')) for i in range(1, 4): shutil.move(Path(tempfile.gettempdir(), f'{i}.wav'), path/f'{i}.wav') diff --git a/core/voice/model/AmazonTts.py b/core/voice/model/AmazonTts.py index 9bedd0c64..6c091349f 100644 --- a/core/voice/model/AmazonTts.py +++ b/core/voice/model/AmazonTts.py @@ -6,6 +6,7 @@ from core.voice.model.Tts import Tts try: + # noinspection PyUnresolvedReferences import boto3 except ModuleNotFoundError: pass # Auto installeed @@ -26,6 +27,7 @@ def __init__(self, user: User = None): self._online = True self._privacyMalus = -20 self._client = None + self._supportsSSML = True # TODO implement the others # https://docs.aws.amazon.com/polly/latest/dg/voicelist.html @@ -186,7 +188,52 @@ def __init__(self, user: User = None): 'Bianca': { 'neural': False }, - 'Carla' : { + 'Carla' : { + 'neural': False + } + } + }, + 'pl-PL': { + 'male' : { + 'Jacek': { + 'neural': False + }, + 'Jan' : { + 'neural': False + } + }, + 'female': { + 'Ewa' : { + 'neural': False + }, + 'Maja': { + 'neural': False + } + } + }, + 'pt-BR': { + 'male' : { + 'Ricardo': { + 'neural': False + } + }, + 'female': { + 'Camila' : { + 'neural': False + }, + 'Vitoria': { + 'neural': False + } + } + }, + 'pt-PT': { + 'male' : { + 'Cristiano': { + 'neural': False + } + }, + 'female': { + 'Ines': { 'neural': False } } @@ -209,12 +256,11 @@ def getWhisperMarkup() -> tuple: return '', '' - @staticmethod - def _checkText(session: DialogSession) -> str: - text = session.payload['text'] + def _checkText(self, session: DialogSession) -> str: + text = super()._checkText(session) - if not re.search('', text): - text = f'{text}' + if not re.search('', text): + text = re.sub(r'(.*)', r'\1', text) return text diff --git a/core/voice/model/GoogleTts.py b/core/voice/model/GoogleTts.py index b3e244425..354eb3b50 100644 --- a/core/voice/model/GoogleTts.py +++ b/core/voice/model/GoogleTts.py @@ -7,7 +7,9 @@ from core.voice.model.Tts import Tts try: + # noinspection PyUnresolvedReferences,PyPackageRequirements from google.oauth2.service_account import Credentials + # noinspection PyUnresolvedReferences,PyPackageRequirements from google.cloud import texttospeech except: pass # Auto installed @@ -29,6 +31,7 @@ def __init__(self, user: User = None): self._online = True self._privacyMalus = -20 self._client = None + self._supportsSSML = True # TODO implement the others # https://cloud.google.com/text-to-speech/docs/voices @@ -142,13 +145,49 @@ def __init__(self, user: User = None): 'it-IT-Standard-A': { 'neural': False }, - 'it-IT-Standard-B' : { + 'it-IT-Standard-B': { 'neural': False }, 'it-IT-Wavenet-A' : { 'neural': True }, - 'it-IT-Wavenet-B': { + 'it-IT-Wavenet-B' : { + 'neural': True + } + } + }, + 'pl-PL': { + 'male' : { + 'pl-PL-Standard-B': { + 'neural': False + }, + 'pl-PL-Standard-C': { + 'neural': False + }, + 'pl-PL-Wavenet-B' : { + 'neural': True + }, + 'pl-PL-Wavenet-C' : { + 'neural': True + } + }, + 'female': { + 'pl-PL-Standard-A': { + 'neural': False + }, + 'pl-PL-Standard-D': { + 'neural': False + }, + 'pl-PL-Standard-E': { + 'neural': True + }, + 'pl-PL-Wavenet-A' : { + 'neural': True + }, + 'pl-PL-Wavenet-D' : { + 'neural': True + }, + 'pl-PL-Wavenet-E' : { 'neural': True } } @@ -163,16 +202,6 @@ def onStart(self): ) - @staticmethod - def _checkText(session: DialogSession) -> str: - text = session.payload['text'] - - if not '' in text: - text = f'{text}' - - return text - - def onSay(self, session: DialogSession): super().onSay(session) diff --git a/core/voice/model/MycroftTts.py b/core/voice/model/MycroftTts.py index 763eafc59..b1157b1dc 100644 --- a/core/voice/model/MycroftTts.py +++ b/core/voice/model/MycroftTts.py @@ -107,12 +107,6 @@ def installDependencies(self) -> bool: return True - @staticmethod - def _checkText(session: DialogSession) -> str: - text = session.payload['text'] - return ' '.join(re.sub('<.*?>', ' ', text).split()) - - def onSay(self, session: DialogSession): super().onSay(session) diff --git a/core/voice/model/PicoTts.py b/core/voice/model/PicoTts.py index 61835dc2f..5f4ccfa96 100644 --- a/core/voice/model/PicoTts.py +++ b/core/voice/model/PicoTts.py @@ -60,12 +60,6 @@ def __init__(self, user: User = None): } - @staticmethod - def _checkText(session: DialogSession) -> str: - text = session.payload['text'] - return ' '.join(re.sub('<.*?>', ' ', text).split()) - - def onSay(self, session: DialogSession): super().onSay(session) diff --git a/core/voice/model/Tts.py b/core/voice/model/Tts.py index c70094c68..98fbc4e40 100644 --- a/core/voice/model/Tts.py +++ b/core/voice/model/Tts.py @@ -1,6 +1,8 @@ import getpass +import re import uuid from pathlib import Path +from re import Match from typing import Optional import hashlib @@ -17,6 +19,7 @@ class Tts(ProjectAliceObject): TEMP_ROOT = Path(tempfile.gettempdir(), '/tempTTS') TTS = None + SPELL_OUT = re.compile(r'(.+)') def __init__(self, user: User = None, *args, **kwargs): @@ -37,6 +40,8 @@ def __init__(self, user: User = None, *args, **kwargs): self._text = '' self._speaking = False + self._supportsSSML = False + def onStart(self): if self._user and self._user.ttsLanguage: @@ -160,6 +165,8 @@ def _speak(self, file: Path, session: DialogSession): duration = round(len(AudioSegment.from_file(file)) / 1000, 2) except CouldntDecodeError: self.logError('Error decoding TTS file') + file.unlink() + self.onSay(session) else: self.DialogManager.increaseSessionTimeout(session=session, interval=duration + 0.2) self.ThreadManager.doLater(interval=duration + 0.1, func=self._sayFinished, args=[session, uid]) @@ -177,9 +184,21 @@ def _sayFinished(self, session: DialogSession, uid: str): ) + def _checkText(self, session: DialogSession) -> str: + text = session.payload['text'] + if not self._supportsSSML: + # We need to remove all ssml tags but transform some first + text = re.sub(self.SPELL_OUT, self._replaceSpellOuts, text) + return ' '.join(re.sub('<.*?>', ' ', text).split()) + else: + if not '' in text: + text = f'{text}' + return text + + @staticmethod - def _checkText(session: DialogSession) -> str: - return session.payload['text'] + def _replaceSpellOuts(matching: Match) -> str: + return ''.join(matching.group(1)) def onSay(self, session: DialogSession): diff --git a/core/voice/model/WatsonTts.py b/core/voice/model/WatsonTts.py index b63dc18d3..a12201845 100644 --- a/core/voice/model/WatsonTts.py +++ b/core/voice/model/WatsonTts.py @@ -25,6 +25,7 @@ def __init__(self, user: User = None): self._online = True self._privacyMalus = -20 self._client = None + self._supportsSSML = True # TODO implement the others # https://cloud.ibm.com/apidocs/text-to-speech?code=python#list-voices @@ -117,16 +118,6 @@ def onStart(self): self._client.set_service_url(self.ConfigManager.getAliceConfigByName('ibmCloudAPIURL')) - @staticmethod - def _checkText(session: DialogSession) -> str: - text = session.payload['text'] - - if not '' in text: - text = f'{text}' - - return text - - def onSay(self, session: DialogSession): super().onSay(session) diff --git a/jetbrainsDebuggers.run/Validate skills.run.xml b/jetbrainsDebuggers.run/Validate skills.run.xml index 8c9598274..66008f130 100644 --- a/jetbrainsDebuggers.run/Validate skills.run.xml +++ b/jetbrainsDebuggers.run/Validate skills.run.xml @@ -1,26 +1,24 @@ - - - - + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1fe6d888b..f312451e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,4 +24,6 @@ PyAudio==0.2.11 pyjwt==1.7.1 toml==0.10.1 markdown==3.2.2 +ProjectAlice-sk #pyopenssl==19.1.0 +sounddevice==0.4.1 diff --git a/requirements_test.txt b/requirements_test.txt index 79392e84a..e9cba619e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -27,3 +27,4 @@ webrtcvad~=2.0.10 pyjwt~=1.7.1 toml~=0.10.1 markdown~=3.2.2 +sounddevice==0.4.1 diff --git a/system/manager/LanguageManager/strings.json b/system/manager/LanguageManager/strings.json index 720acae16..af604e6d3 100644 --- a/system/manager/LanguageManager/strings.json +++ b/system/manager/LanguageManager/strings.json @@ -11,6 +11,10 @@ ], "it": [ "fuori" + ], + "pl": [ + "na zewnątrz", + "na dworze" ] }, "intentSeparator": { @@ -25,6 +29,9 @@ ], "it": [ " ed anche " + ], + "pl": [ + " a także " ] }, "inThe": { @@ -53,6 +60,11 @@ "nei ", "dentro ", "in " + ], + "pl": [ + "w pokoju ", + "w ", + "w pomieszczeniu " ] }, "everywhere": { @@ -68,6 +80,9 @@ "it": [ "dappertutto", "ovunque" + ], + "pl": [ + "wszędzie" ] }, "+": { @@ -82,6 +97,9 @@ ], "it": [ "più" + ], + "pl": [ + "plus" ] }, "-": { @@ -96,6 +114,9 @@ ], "it": [ "meno" + ], + "pl": [ + "minus" ] }, "*": { @@ -110,6 +131,9 @@ ], "it": [ "per" + ], + "pl": [ + "razy" ] }, "/": { @@ -124,6 +148,9 @@ ], "it": [ "diviso" + ], + "pl": [ + "dzielone przez" ] }, "%": { @@ -138,6 +165,9 @@ ], "it": [ "modulo" + ], + "en": [ + "modulo" ] }, "cancelIntent": { @@ -165,6 +195,38 @@ "annulla", "sta 'zitto", "silenzio" + ], + "pl": [ + "anuluj", + "stop", + "zamknij się", + "cisza", + "cicho bądź", + "bądź cicho", + "morda w kubeł", + "ucisz się" + ] + }, + "politness": { + "en": [ + "please", + "could you", + "would you be so nice" + ], + "fr": [ + "s'il te plaît", + "pourrais-tu" + ], + "de": [ + "bitte", + "danke", + "kannst du", + "wärst du so nett", + "würdest du", + "könntest du" + ], + "it": [ + "per favore" ] } } diff --git a/system/manager/TalkManager/talks/pl.json b/system/manager/TalkManager/talks/pl.json new file mode 100644 index 000000000..874143a25 --- /dev/null +++ b/system/manager/TalkManager/talks/pl.json @@ -0,0 +1,93 @@ + { + "noAccess": { + "default": [ + "Przepraszam, nie mogę. Nie masz wystarczających uprawnień", + "Niemożliwe. Twoje uprawnienia są za niskie" + ], + "short":[ + "Brak dostępu" + ] + }, + "unknowUser": { + "default": [ + "Przepraszam ale nie wiem kim jesteś", + "Przepraszam, nie mogę. Nie znam cię", + "Musisz zwracać się do mnie po imieniu abym wiedział kim jesteś" + ], + "short": [ + "Nie znam cię" + ] + }, + "notUnderstood": { + "default": [ + "Przepraszam ale nie jestem pewien czy dobrze rozumiem czego chcesz", + "Nie zrozumiełem czego chcesz", + "Przepraszam, czy możesz powtórzyć?", + "Co??" + ], + "short": [ + "Nie zrozumiałem" + ] + }, + "notUnderstoodEnd": { + "default": [ + "Przepraszam ale Cię nie rozumiem", + "Przepraszam ale nie rozumiem czego chcesz", + "Koniec tematu. Nie rozumiem Twojego polecenia", + "Stop. Nic z tego nie rozumiem" + ], + "short": [ + "Nie rozumiem" + ] + }, + "youreWelcome": { + "default": [ + "Proszę bardzo!", + "Cała przyjemność po mojej stronie!", + "Nie ma sprawy!", + "W porządku!" + ] + }, + "error": { + "default": [ + "Coś poszło nie tak, sprawdź logi" + ] + }, + "newDeviceAdditionFailed": { + "default": [ + "Przepraszam, pojawił się problem z dodaniem nowego urządzenia" + ] + }, + "newDeviceAdditionSuccess": { + "default": [ + "Nowe urządzenie zostało znalezione i pomyślnie dodane!" + ] + }, + "maxOneAlicePerRoom": { + "default": [ + "Przepraszam ale nie możesz mieć więcej niż jeden odbiornik w pomieszczeniu", + "Przepraszam ale w jednym pomieszczeniu może być tylko jeden odbiornik" + ] + }, + "wakewordCaptured": [ + "Zarejestrowałem twoje wyrażenie wywołujące, sprawdźmy je", + "OK, zarejestrowane. Odtwarzam do weryfikacji", + "Zarejestrowane. Posłuchaj czy wszystko się zgadza" + ], + "isWakewordSampleOk": [ + "Sprawdź czy tak jest dobrze" + ], + "offline": [ + "Przepraszam ale nie mogę tego zrobić będąc w trybie offline" + ], + "notification": [ + "Tak?", + "Słucham?", + "Zamieniam się w słuch!", + "Do usług!", + "Czekam na polecenie", + "Czekam na rozkaz", + "No co tam?", + "Mów o co chodzi" + ] +} diff --git a/system/node-red/settings.js b/system/node-red/settings.js new file mode 100644 index 000000000..091767c76 --- /dev/null +++ b/system/node-red/settings.js @@ -0,0 +1,306 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * 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. + **/ + +module.exports = { + // the tcp port that the Node-RED web server is listening on + uiPort: process.env.PORT || 1880, + + // By default, the Node-RED UI accepts connections on all IPv4 interfaces. + // To listen on all IPv6 addresses, set uiHost to "::", + // The following property can be used to listen on a specific interface. For + // example, the following would only allow connections from the local machine. + //uiHost: "127.0.0.1", + + // Retry time in milliseconds for MQTT connections + mqttReconnectTime: 15000, + + // Retry time in milliseconds for Serial port connections + serialReconnectTime: 15000, + + // Retry time in milliseconds for TCP socket connections + //socketReconnectTime: 10000, + + // Timeout in milliseconds for TCP server socket connections + // defaults to no timeout + //socketTimeout: 120000, + + // Maximum number of messages to wait in queue while attempting to connect to TCP socket + // defaults to 1000 + //tcpMsgQueueSize: 2000, + + // Timeout in milliseconds for HTTP request connections + // defaults to 120 seconds + //httpRequestTimeout: 120000, + + // The maximum length, in characters, of any message sent to the debug sidebar tab + debugMaxLength: 1000, + + // The maximum number of messages nodes will buffer internally as part of their + // operation. This applies across a range of nodes that operate on message sequences. + // defaults to no limit. A value of 0 also means no limit is applied. + //nodeMessageBufferMaxLength: 0, + + // To disable the option for using local files for storing keys and certificates in the TLS configuration + // node, set this to true + //tlsConfigDisableLocalFiles: true, + + // Colourise the console output of the debug node + //debugUseColors: true, + + // The file containing the flows. If not set, it defaults to flows_.json + //flowFile: 'flows.json', + + // To enabled pretty-printing of the flow within the flow file, set the following + // property to true: + //flowFilePretty: true, + + // By default, credentials are encrypted in storage using a generated key. To + // specify your own secret, set the following property. + // If you want to disable encryption of credentials, set this property to false. + // Note: once you set this property, do not change it - doing so will prevent + // node-red from being able to decrypt your existing credentials and they will be + // lost. + //credentialSecret: "a-secret-key", + + // By default, all user data is stored in a directory called `.node-red` under + // the user's home directory. To use a different location, the following + // property can be used + //userDir: '/home/nol/.node-red/', + + // Node-RED scans the `nodes` directory in the userDir to find local node files. + // The following property can be used to specify an additional directory to scan. + //nodesDir: '/home/nol/.node-red/nodes', + + // By default, the Node-RED UI is available at http://localhost:1880/ + // The following property can be used to specify a different root path. + // If set to false, this is disabled. + //httpAdminRoot: '/admin', + + // Some nodes, such as HTTP In, can be used to listen for incoming http requests. + // By default, these are served relative to '/'. The following property + // can be used to specifiy a different root path. If set to false, this is + // disabled. + //httpNodeRoot: '/red-nodes', + + // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot', + // to apply the same root to both parts. + //httpRoot: '/red', + + // When httpAdminRoot is used to move the UI to a different root path, the + // following property can be used to identify a directory of static content + // that should be served at http://localhost:1880/. + //httpStatic: '/home/nol/node-red-static/', + + // The maximum size of HTTP request that will be accepted by the runtime api. + // Default: 5mb + //apiMaxLength: '5mb', + + // If you installed the optional node-red-dashboard you can set it's path + // relative to httpRoot + //ui: { path: "ui" }, + + // Securing Node-RED + // ----------------- + // To password protect the Node-RED editor and admin API, the following + // property can be used. See http://nodered.org/docs/security.html for details. + //adminAuth: { + // type: "credentials", + // users: [{ + // username: "admin", + // password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.", + // permissions: "*" + // }] + //}, + + // To password protect the node-defined HTTP endpoints (httpNodeRoot), or + // the static content (httpStatic), the following properties can be used. + // The pass field is a bcrypt hash of the password. + // See http://nodered.org/docs/security.html#generating-the-password-hash + //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + + // The following property can be used to enable HTTPS + // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + // for details on its contents. + // This property can be either an object, containing both a (private) key and a (public) certificate, + // or a function that returns such an object: + //// https object: + //https: { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + //}, + ////https function: + // https: function() { + // // This function should return the options object, or a Promise + // // that resolves to the options object + // return { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // } + // }, + + // The following property can be used to refresh the https settings at a + // regular time interval in hours. + // This requires: + // - the `https` setting to be a function that can be called to get + // the refreshed settings. + // - Node.js 11 or later. + //httpsRefreshInterval : 12, + + // The following property can be used to cause insecure HTTP connections to + // be redirected to HTTPS. + //requireHttps: true, + + // The following property can be used to disable the editor. The admin API + // is not affected by this option. To disable both the editor and the admin + // API, use either the httpRoot or httpAdminRoot properties + //disableEditor: false, + + // The following property can be used to configure cross-origin resource sharing + // in the HTTP nodes. + // See https://github.com/troygoode/node-cors#configuration-options for + // details on its contents. The following is a basic permissive set of options: + //httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + //}, + + // If you need to set an http proxy please set an environment variable + // called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. + // For example - http_proxy=http://myproxy.com:8080 + // (Setting it here will have no effect) + // You may also specify no_proxy (or NO_PROXY) to supply a comma separated + // list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk + + // The following property can be used to add a custom middleware function + // in front of all http in nodes. This allows custom authentication to be + // applied to all http in nodes, or any other sort of common request processing. + //httpNodeMiddleware: function(req,res,next) { + // // Handle/reject the request, or pass it on to the http in node by calling next(); + // // Optionally skip our rawBodyParser by setting this to true; + // //req.skipRawBodyParser = true; + // next(); + //}, + + + // The following property can be used to add a custom middleware function + // in front of all admin http routes. For example, to set custom http + // headers + // httpAdminMiddleware: function(req,res,next) { + // // Set the X-Frame-Options header to limit where the editor + // // can be embedded + // //res.set('X-Frame-Options', 'sameorigin'); + // next(); + // }, + + // The following property can be used to pass custom options to the Express.js + // server used by Node-RED. For a full list of available options, refer + // to http://expressjs.com/en/api.html#app.settings.table + //httpServerOptions: { }, + + // The following property can be used to verify websocket connection attempts. + // This allows, for example, the HTTP request headers to be checked to ensure + // they include valid authentication information. + //webSocketNodeVerifyClient: function(info) { + // // 'info' has three properties: + // // - origin : the value in the Origin header + // // - req : the HTTP request + // // - secure : true if req.connection.authorized or req.connection.encrypted is set + // // + // // The function should return true if the connection should be accepted, false otherwise. + // // + // // Alternatively, if this function is defined to accept a second argument, callback, + // // it can be used to verify the client asynchronously. + // // The callback takes three arguments: + // // - result : boolean, whether to accept the connection or not + // // - code : if result is false, the HTTP error status to return + // // - reason: if result is false, the HTTP reason string to return + //}, + + // The following property can be used to seed Global Context with predefined + // values. This allows extra node modules to be made available with the + // Function node. + // For example, + // functionGlobalContext: { os:require('os') } + // can be accessed in a function block as: + // global.get("os") + functionGlobalContext : { + // os:require('os'), + // jfive:require("johnny-five"), + // j5board:require("johnny-five").Board({repl:false}) + }, + // `global.keys()` returns a list of all properties set in global context. + // This allows them to be displayed in the Context Sidebar within the editor. + // In some circumstances it is not desirable to expose them to the editor. The + // following property can be used to hide any property set in `functionGlobalContext` + // from being list by `global.keys()`. + // By default, the property is set to false to avoid accidental exposure of + // their values. Setting this to true will cause the keys to be listed. + exportGlobalContextKeys: false, + + + // Context Storage + // The following property can be used to enable context storage. The configuration + // provided here will enable file-based context that flushes to disk every 30 seconds. + // Refer to the documentation for further options: https://nodered.org/docs/api/context/ + // + //contextStorage: { + // default: { + // module:"localfilesystem" + // }, + //}, + + // The following property can be used to order the categories in the editor + // palette. If a node's category is not in the list, the category will get + // added to the end of the palette. + // If not set, the following default order is used: + //paletteCategories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], + + // Configure the logging output + logging: { + // Only console logging is currently supported + console: { + // Level of logging to be recorded. Options are: + // fatal - only those errors which make the application unusable should be recorded + // error - record errors which are deemed fatal for a particular request + fatal errors + // warn - record problems which are non fatal + errors + fatal errors + // info - record information about the general running of the application + warn + error + fatal errors + // debug - record information which is more verbose than info + info + warn + error + fatal errors + // trace - record very detailed logging + debug + info + warn + error + fatal errors + // off - turn off all logging (doesn't affect metrics or audit) + level : 'info', + // Whether or not to include metric events in the log output + metrics: false, + // Whether or not to include audit events in the log output + audit : false + } + }, + + // Customising the editor + editorTheme: { + page : { + title : 'Project Alice Node-RED', + favicon: '~/ProjectAlice/core/interface/static/favicon.ico', + css : [ + '/home/pi/.node-red/node_modules/@node-red-contrib-themes/midnight-red/theme.css', + '/home/pi/.node-red/node_modules/@node-red-contrib-themes/midnight-red/scrollbars.css' + ] + }, + projects: { + enabled: false + } + } +}; diff --git a/tests/commons/test_CommonsManager.py b/tests/commons/test_CommonsManager.py index ca452dbc0..1069b43d3 100644 --- a/tests/commons/test_CommonsManager.py +++ b/tests/commons/test_CommonsManager.py @@ -1,6 +1,7 @@ import unittest from unittest import mock from unittest.mock import MagicMock +from uuid import UUID from core.commons.CommonsManager import CommonsManager @@ -296,5 +297,37 @@ def test_indexOf(self): self.assertEqual(CommonsManager.indexOf('nn', string3), 1) + def test_isUuid(self): + validStrings = [ + '6e9bb2f8-bedb-4ade-9db7-f455e4c03051', + '{6e9bb2f8-bedb-4ade-9db7-f455e4c03051}', + '6e9bb2f8-becb-4ade-9db7-f455e4c03051', + '6e9bb2f8bedb4ade9db7f455e4c03051', + 'urn:uuid:6e9bb2f8bedb4ade9db7f455e4c03051' + ] + invalidStrings = [ + '{6e9bb2f8-begb-4ade-9db7-f455e4c03051}', + '6e9bb2f8-bedb-4ade-9db7-f455e4c030', + 'unittests' + ] + + for string in validStrings: + try: + val = UUID(string) + except: + val = None + + self.assertIsNotNone(val) + + + for string in invalidStrings: + try: + val = UUID(string) + except: + val = None + + self.assertIsNone(val) + + if __name__ == '__main__': unittest.main() diff --git a/tests/util/test_InternetManager.py b/tests/util/test_InternetManager.py index 93dd54b1d..7c2ff6270 100644 --- a/tests/util/test_InternetManager.py +++ b/tests/util/test_InternetManager.py @@ -12,11 +12,17 @@ class TestInternetManager(unittest.TestCase): @mock.patch('core.util.InternetManager.Manager.broadcast') @mock.patch('core.util.InternetManager.requests') @mock.patch('core.util.InternetManager.InternetManager.Commons', new_callable=PropertyMock) - def test_checkOnlineState(self, mock_commons, mock_requests, mock_broadcast): + @mock.patch('core.base.SuperManager.SuperManager') + def test_checkOnlineState(self, mock_superManager, mock_commons, mock_requests, mock_broadcast): common_mock = MagicMock() common_mock.getFunctionCaller.return_value = 'InternetManager' mock_commons.return_value = common_mock + # mock SuperManager + mock_instance = MagicMock() + mock_superManager.getInstance.return_value = mock_instance + mock_instance.configManager.getAliceConfigByName.return_value = False + internetManager = InternetManager() # request returns status code 204 @@ -25,7 +31,14 @@ def test_checkOnlineState(self, mock_commons, mock_requests, mock_broadcast): type(mock_requestsResult).status_code = mock_statusCode mock_requests.get.return_value = mock_requestsResult + # Not called if stay completly offline + mock_instance.configManager.getAliceConfigByName.return_value = True + internetManager.checkOnlineState() + mock_requests.get.asset_not_called() + + mock_instance.configManager.getAliceConfigByName.return_value = False internetManager.checkOnlineState() + mock_requests.get.assert_called_once_with('https://clients3.google.com/generate_204') mock_broadcast.assert_called_once_with(method='internetConnected', exceptions=['InternetManager'], propagateToSkills=True) self.assertEqual(internetManager.online, True)