Updated HACS and also fixed Garadget #727

This commit is contained in:
ccostan
2020-04-09 21:29:27 -04:00
parent 51aab60dea
commit e6e0d442e9
65 changed files with 1510 additions and 1047 deletions

0
config/custom_components/hacs/.translations/da.json Executable file → Normal file
View File

14
config/custom_components/hacs/.translations/de.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integration", "integration": "Integration",
"integrations": "Integrationen", "integrations": "Integrationen",
"manage": "verwalten", "manage": "verwalten",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Apps",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Python Skript", "python_script": "Python Skript",
@@ -33,11 +35,12 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"appdaemon": "AppDaemon App-Discovery & Tracking aktivieren", "appdaemon": "AppDaemon App-Entdeckung & Nachverfolgung aktivieren",
"python_script": "Python Script-Discovery & Tracking aktivieren", "netdaemon": "NetDaemon App-Entdeckung & Nachverfolgung aktivieren",
"python_script": "Python Script-Entdeckung & Nachverfolgung aktivieren",
"sidepanel_icon": "Sidepanel Symbol", "sidepanel_icon": "Sidepanel Symbol",
"sidepanel_title": "Sidepanel Titel", "sidepanel_title": "Sidepanel Titel",
"theme": "Theme-Discovery & Tracking aktivieren", "theme": "Theme-Entdeckung & Nachverfolgung aktivieren",
"token": "Persönlicher GitHub Zugriffstoken" "token": "Persönlicher GitHub Zugriffstoken"
}, },
"description": "Wenn du Hilfe mit den Einstellungen brauchst, kannst du hier nachsehen: https:\/\/hacs.xyz\/docs\/configuration\/start", "description": "Wenn du Hilfe mit den Einstellungen brauchst, kannst du hier nachsehen: https:\/\/hacs.xyz\/docs\/configuration\/start",
@@ -56,6 +59,7 @@
"exist": "{item} existiert bereits", "exist": "{item} existiert bereits",
"generic": "Bist du dir sicher?", "generic": "Bist du dir sicher?",
"home_assistant_is_restarting": "Bitte warte, Home Assistant wird jetzt neu gestartet.", "home_assistant_is_restarting": "Bitte warte, Home Assistant wird jetzt neu gestartet.",
"home_assistant_version_not_correct": "Du benutzt die Home Assistant-Version '{haversion}', für dieses Repository muss jedoch die Mindestversion '{minversion}' installiert sein.",
"no": "Nein", "no": "Nein",
"no_upgrades": "Keine Upgrades ausstehend", "no_upgrades": "Keine Upgrades ausstehend",
"ok": "OK", "ok": "OK",
@@ -70,10 +74,11 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"appdaemon": "AppDaemon App-Discovery & Tracking aktivieren", "appdaemon": "AppDaemon App-Entdeckung & Nachverfolgung aktivieren",
"country": "Nach Ländercode filtern.", "country": "Nach Ländercode filtern.",
"debug": "Debug aktivieren.", "debug": "Debug aktivieren.",
"experimental": "Experimentelle Funktionen aktivieren", "experimental": "Experimentelle Funktionen aktivieren",
"netdaemon": "NetDaemon App-Entdeckung & Nachverfolgung aktivieren",
"not_in_use": "Nicht in Verwendung mit YAML", "not_in_use": "Nicht in Verwendung mit YAML",
"release_limit": "Anzahl anzuzeigender Releases.", "release_limit": "Anzahl anzuzeigender Releases.",
"sidepanel_icon": "Sidepanel Symbol", "sidepanel_icon": "Sidepanel Symbol",
@@ -115,6 +120,7 @@
"note_installed": "Wird installiert nach", "note_installed": "Wird installiert nach",
"note_integration": "du musst es dann noch in die Datei 'configuration.yaml' hinzufügen", "note_integration": "du musst es dann noch in die Datei 'configuration.yaml' hinzufügen",
"note_plugin": "du musst es dann noch in deine Lovelace-Einstellungen ('ui-lovelace.yaml' oder im Raw-Konfigurationseditor) hinzufügen", "note_plugin": "du musst es dann noch in deine Lovelace-Einstellungen ('ui-lovelace.yaml' oder im Raw-Konfigurationseditor) hinzufügen",
"note_plugin_post_107": "Du musst es noch zu deiner Lovelace-Konfiguration hinzufügen ('configuration.yaml' oder der Ressourceneditor '\/config\/lovelace\/resources')",
"open_issue": "Problem melden", "open_issue": "Problem melden",
"open_plugin": "Plugin öffnen", "open_plugin": "Plugin öffnen",
"reinstall": "Neu installieren", "reinstall": "Neu installieren",

7
config/custom_components/hacs/.translations/el.json Executable file → Normal file
View File

@@ -5,12 +5,15 @@
"appdaemon_apps": "AppDaemon Apps", "appdaemon_apps": "AppDaemon Apps",
"background_task": "Τρέχει μια διεργασία στο παρασκήνιο, η σελίδα θα ανανεωθεί μόλις αυτό ολοκληρωθεί.", "background_task": "Τρέχει μια διεργασία στο παρασκήνιο, η σελίδα θα ανανεωθεί μόλις αυτό ολοκληρωθεί.",
"check_log_file": "Ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες.", "check_log_file": "Ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες.",
"continue": "Να συνεχίσει",
"disabled": "Απενεργοποιημένο", "disabled": "Απενεργοποιημένο",
"documentation": "Τεκμηρίωση", "documentation": "Τεκμηρίωση",
"hacs_is_disabled": "Το HACS είναι απενεργοποιημένο", "hacs_is_disabled": "Το HACS είναι απενεργοποιημένο",
"installed": "εγκατεστημένο", "installed": "εγκατεστημένο",
"integration": "Ενσωμάτωση", "integration": "Ενσωμάτωση",
"integrations": "Ενσωματωμένα", "integrations": "Ενσωματωμένα",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Apps",
"plugin": "Πρόσθετο", "plugin": "Πρόσθετο",
"plugins": "Πρόσθετα", "plugins": "Πρόσθετα",
"python_script": "Πρόγραμμα Python", "python_script": "Πρόγραμμα Python",
@@ -32,6 +35,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το AppDaemon", "appdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το AppDaemon",
"netdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το NetDaemon",
"python_script": "Ενεργοποίηση εύρεσης & παρακολούθησης για τα python_scripts", "python_script": "Ενεργοποίηση εύρεσης & παρακολούθησης για τα python_scripts",
"sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ", "sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ",
"sidepanel_title": "Τίτλος πλαϊνού πάνελ", "sidepanel_title": "Τίτλος πλαϊνού πάνελ",
@@ -50,6 +54,7 @@
"cancel": "Ακύρωση", "cancel": "Ακύρωση",
"continue": "Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", "continue": "Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
"delete": "Είστε σίγουροι ότι θέλετε να διαγράψετε το '{item}';", "delete": "Είστε σίγουροι ότι θέλετε να διαγράψετε το '{item}';",
"delete_installed": "Το '{item}' είναι εγκατεστημένο, πρέπει να το απεγκαταστήσετε πριν να το διαγράψετε.",
"exist": "{item} υπάρχει ήδη", "exist": "{item} υπάρχει ήδη",
"generic": "Είστε βέβαιοι;", "generic": "Είστε βέβαιοι;",
"home_assistant_is_restarting": "Περιμένετε, το Home Assistant επανεκκινείται τώρα.", "home_assistant_is_restarting": "Περιμένετε, το Home Assistant επανεκκινείται τώρα.",
@@ -70,6 +75,7 @@
"country": "Κριτήριο με βάση τον κωδικό χώρας.", "country": "Κριτήριο με βάση τον κωδικό χώρας.",
"debug": "Ενεργοποίηση εντοπισμού σφαλμάτων.", "debug": "Ενεργοποίηση εντοπισμού σφαλμάτων.",
"experimental": "Ενεργοποίση πειραματικών λειτουργιών", "experimental": "Ενεργοποίση πειραματικών λειτουργιών",
"netdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το NetDaemon",
"release_limit": "Αριθμός εκδόσεων που να παραθέτονται.", "release_limit": "Αριθμός εκδόσεων που να παραθέτονται.",
"sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ", "sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ",
"sidepanel_title": "Τίτλος πλαϊνού πάνελ" "sidepanel_title": "Τίτλος πλαϊνού πάνελ"
@@ -110,6 +116,7 @@
"open_plugin": "Ανοιχτό πρόσθετο", "open_plugin": "Ανοιχτό πρόσθετο",
"reinstall": "Επανεγκατάσταση", "reinstall": "Επανεγκατάσταση",
"repository": "Αποθετήριο", "repository": "Αποθετήριο",
"restart_home_assistant": "Επανεκκίνηση του Home Assistant",
"show_beta": "Εμφάνιση του beta", "show_beta": "Εμφάνιση του beta",
"uninstall": "Απεγκατάσταση", "uninstall": "Απεγκατάσταση",
"update_information": "Ενημέρωση πληροφοριών", "update_information": "Ενημέρωση πληροφοριών",

5
config/custom_components/hacs/.translations/en.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integration", "integration": "Integration",
"integrations": "Integrations", "integrations": "Integrations",
"manage": "manage", "manage": "manage",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Apps",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Python Script", "python_script": "Python Script",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Enable AppDaemon apps discovery & tracking", "appdaemon": "Enable AppDaemon apps discovery & tracking",
"netdaemon": "Enable NetDaemon apps discovery & tracking",
"python_script": "Enable python_scripts discovery & tracking", "python_script": "Enable python_scripts discovery & tracking",
"sidepanel_icon": "Side panel icon", "sidepanel_icon": "Side panel icon",
"sidepanel_title": "Side panel title", "sidepanel_title": "Side panel title",
@@ -75,6 +78,7 @@
"country": "Filter with country code.", "country": "Filter with country code.",
"debug": "Enable debug.", "debug": "Enable debug.",
"experimental": "Enable experimental features", "experimental": "Enable experimental features",
"netdaemon": "Enable NetDaemon apps discovery & tracking",
"not_in_use": "Not in use with YAML", "not_in_use": "Not in use with YAML",
"release_limit": "Number of releases to show.", "release_limit": "Number of releases to show.",
"sidepanel_icon": "Side panel icon", "sidepanel_icon": "Side panel icon",
@@ -116,6 +120,7 @@
"note_installed": "When installed, this will be located in", "note_installed": "When installed, this will be located in",
"note_integration": "you still need to add it to your 'configuration.yaml' file", "note_integration": "you still need to add it to your 'configuration.yaml' file",
"note_plugin": "you still need to add it to your lovelace configuration ('ui-lovelace.yaml' or the raw UI config editor)", "note_plugin": "you still need to add it to your lovelace configuration ('ui-lovelace.yaml' or the raw UI config editor)",
"note_plugin_post_107": "you still need to add it to your lovelace configuration ('configuration.yaml' or the resource editor '\/config\/lovelace\/resources')",
"open_issue": "Open issue", "open_issue": "Open issue",
"open_plugin": "Open plugin", "open_plugin": "Open plugin",
"reinstall": "Reinstall", "reinstall": "Reinstall",

6
config/custom_components/hacs/.translations/es.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integración", "integration": "Integración",
"integrations": "Integraciones", "integrations": "Integraciones",
"manage": "Administrar", "manage": "Administrar",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Apps",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Python Script", "python_script": "Python Script",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de AppDaemon", "appdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de AppDaemon",
"netdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de NetDaemon",
"python_script": "Habilitar el descubrimiento y seguimiento en python_scripts", "python_script": "Habilitar el descubrimiento y seguimiento en python_scripts",
"sidepanel_icon": "Ícono del panel lateral", "sidepanel_icon": "Ícono del panel lateral",
"sidepanel_title": "Título del panel lateral", "sidepanel_title": "Título del panel lateral",
@@ -56,6 +59,7 @@
"exist": "{item} ya existe", "exist": "{item} ya existe",
"generic": "¿Estás seguro?", "generic": "¿Estás seguro?",
"home_assistant_is_restarting": "Espera, Home Assistant se está reiniciando.", "home_assistant_is_restarting": "Espera, Home Assistant se está reiniciando.",
"home_assistant_version_not_correct": "Está ejecutando la versión '{haversion}' de Home Assistant, pero este repositorio requiere la instalación de la versión '{minversion}' mínima.",
"no": "No", "no": "No",
"no_upgrades": "No hay actualizaciones pendientes", "no_upgrades": "No hay actualizaciones pendientes",
"ok": "OK", "ok": "OK",
@@ -74,6 +78,7 @@
"country": "Filtrar por el código de país.", "country": "Filtrar por el código de país.",
"debug": "Habilitar depuración.", "debug": "Habilitar depuración.",
"experimental": "Habilitar funciones experimentales", "experimental": "Habilitar funciones experimentales",
"netdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de NetDaemon",
"not_in_use": "No usarse con YAML", "not_in_use": "No usarse con YAML",
"release_limit": "Número de versiones a mostrar.", "release_limit": "Número de versiones a mostrar.",
"sidepanel_icon": "Icono del panel lateral", "sidepanel_icon": "Icono del panel lateral",
@@ -84,6 +89,7 @@
}, },
"repository_banner": { "repository_banner": {
"config_flow": "Esta integración soporta config_flow, lo que significa que ahora puede ir a la sección de integración de su UI para configurarlo.", "config_flow": "Esta integración soporta config_flow, lo que significa que ahora puede ir a la sección de integración de su UI para configurarlo.",
"config_flow_title": "Configuración de UI soportada",
"integration_not_loaded": "Esta integración no se carga en Home Assistant.", "integration_not_loaded": "Esta integración no se carga en Home Assistant.",
"no_restart_required": "No es necesario reiniciar", "no_restart_required": "No es necesario reiniciar",
"not_loaded": "No está cargado", "not_loaded": "No está cargado",

4
config/custom_components/hacs/.translations/fr.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Intégration", "integration": "Intégration",
"integrations": "Intégrations", "integrations": "Intégrations",
"manage": "gérer", "manage": "gérer",
"netdaemon": "NetDaemon",
"netdaemon_apps": "Applications NetDaemon",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Script Python", "python_script": "Script Python",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Activer la découverte et le suivi des applications AppDaemon", "appdaemon": "Activer la découverte et le suivi des applications AppDaemon",
"netdaemon": "Activer la découverte et le suivi des applications NetDaemon",
"python_script": "Activer la découverte et le suivi des scripts python", "python_script": "Activer la découverte et le suivi des scripts python",
"sidepanel_icon": "Icône de la barre latérale", "sidepanel_icon": "Icône de la barre latérale",
"sidepanel_title": "Titre de la barre latérale", "sidepanel_title": "Titre de la barre latérale",
@@ -75,6 +78,7 @@
"country": "Filtrer par code pays.", "country": "Filtrer par code pays.",
"debug": "Activez le débogage.", "debug": "Activez le débogage.",
"experimental": "Activer les fonctionnalités expérimentales", "experimental": "Activer les fonctionnalités expérimentales",
"netdaemon": "Activer la découverte et le suivi des applications NetDaemon",
"not_in_use": "Non utilisé avec YAML", "not_in_use": "Non utilisé avec YAML",
"release_limit": "Nombre de recensés à afficher.", "release_limit": "Nombre de recensés à afficher.",
"sidepanel_icon": "Icône de la barre latérale", "sidepanel_icon": "Icône de la barre latérale",

0
config/custom_components/hacs/.translations/hu.json Executable file → Normal file
View File

7
config/custom_components/hacs/.translations/it.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integrazione", "integration": "Integrazione",
"integrations": "Integrazioni", "integrations": "Integrazioni",
"manage": "gestione", "manage": "gestione",
"netdaemon": "NetDaemon",
"netdaemon_apps": "Applicazioni NetDaemon",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugin", "plugins": "Plugin",
"python_script": "Script python", "python_script": "Script python",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni AppDaemon", "appdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni AppDaemon",
"netdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni NetDaemon",
"python_script": "Abilita il rilevamento e il monitoraggio dei python_scripts", "python_script": "Abilita il rilevamento e il monitoraggio dei python_scripts",
"sidepanel_icon": "Icona nel pannello laterale", "sidepanel_icon": "Icona nel pannello laterale",
"sidepanel_title": "Titolo nel pannello laterale", "sidepanel_title": "Titolo nel pannello laterale",
@@ -56,6 +59,7 @@
"exist": "{item} esiste già", "exist": "{item} esiste già",
"generic": "Sei sicuro?", "generic": "Sei sicuro?",
"home_assistant_is_restarting": "Aspetta, Home Assistant si sta riavviando.", "home_assistant_is_restarting": "Aspetta, Home Assistant si sta riavviando.",
"home_assistant_version_not_correct": "Stai eseguendo la versione Home Assistant '{haversion}', ma questo repository richiede l'installazione della versione minima '{minversion}'.",
"no": "No", "no": "No",
"no_upgrades": "Nessun aggiornamento in sospeso", "no_upgrades": "Nessun aggiornamento in sospeso",
"ok": "OK", "ok": "OK",
@@ -74,6 +78,8 @@
"country": "Filtra con prefisso internazionale.", "country": "Filtra con prefisso internazionale.",
"debug": "Abilita debug.", "debug": "Abilita debug.",
"experimental": "Abilita funzionalità sperimentali", "experimental": "Abilita funzionalità sperimentali",
"netdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni NetDaemon",
"not_in_use": "Non in uso con YAML",
"release_limit": "Numero di versioni da mostrare.", "release_limit": "Numero di versioni da mostrare.",
"sidepanel_icon": "Icona nel pannello laterale", "sidepanel_icon": "Icona nel pannello laterale",
"sidepanel_title": "Titolo nel pannello laterale" "sidepanel_title": "Titolo nel pannello laterale"
@@ -83,6 +89,7 @@
}, },
"repository_banner": { "repository_banner": {
"config_flow": "Questa integrazione supporta config_flow, questo significa che è ora possibile passare alla sezione \"IntegrazionI\" dell'interfaccia utente per la configurazione.", "config_flow": "Questa integrazione supporta config_flow, questo significa che è ora possibile passare alla sezione \"IntegrazionI\" dell'interfaccia utente per la configurazione.",
"config_flow_title": "Configurazione dell'interfaccia utente supportata",
"integration_not_loaded": "Questa integrazione non è caricata in Home Assistant.", "integration_not_loaded": "Questa integrazione non è caricata in Home Assistant.",
"no_restart_required": "Non è necessario riavviare", "no_restart_required": "Non è necessario riavviare",
"not_loaded": "Non caricato", "not_loaded": "Non caricato",

4
config/custom_components/hacs/.translations/nb.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integrasjon", "integration": "Integrasjon",
"integrations": "Integrasjoner", "integrations": "Integrasjoner",
"manage": "manage", "manage": "manage",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Apper",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Python-skript", "python_script": "Python-skript",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Aktiver oppdagelse og sporing av AppDaemon-apper", "appdaemon": "Aktiver oppdagelse og sporing av AppDaemon-apper",
"netdaemon": "Aktiver oppdagelse og sporing av NetDaemon-apper",
"python_script": "Aktiver oppdagelse og sporing av python_scripts", "python_script": "Aktiver oppdagelse og sporing av python_scripts",
"sidepanel_icon": "Sidepanel ikon", "sidepanel_icon": "Sidepanel ikon",
"sidepanel_title": "Sidepanel tittel", "sidepanel_title": "Sidepanel tittel",
@@ -75,6 +78,7 @@
"country": "Filtrer med landskode.", "country": "Filtrer med landskode.",
"debug": "Aktiver debug", "debug": "Aktiver debug",
"experimental": "Aktiver eksperimentelle funksjoner", "experimental": "Aktiver eksperimentelle funksjoner",
"netdaemon": "Aktiver oppdagelse og sporing av NetDaemon-apper",
"not_in_use": "Ikke i bruk med YAML", "not_in_use": "Ikke i bruk med YAML",
"release_limit": "Antall utgivelser som skal vises.", "release_limit": "Antall utgivelser som skal vises.",
"sidepanel_icon": "Sidepanel ikon", "sidepanel_icon": "Sidepanel ikon",

5
config/custom_components/hacs/.translations/nl.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integratie", "integration": "Integratie",
"integrations": "Integraties", "integrations": "Integraties",
"manage": "beheer", "manage": "beheer",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Apps",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Python Script", "python_script": "Python Script",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Zet AppDaemon apps ontdekken & traceren aan", "appdaemon": "Zet AppDaemon apps ontdekken & traceren aan",
"netdaemon": "Zet NetDaemon apps ontdekken & traceren aan",
"python_script": "Zet python_scripts ontdekken & traceren aan", "python_script": "Zet python_scripts ontdekken & traceren aan",
"sidepanel_icon": "Zijpaneel icoon", "sidepanel_icon": "Zijpaneel icoon",
"sidepanel_title": "Zijpaneel titel", "sidepanel_title": "Zijpaneel titel",
@@ -74,6 +77,7 @@
"country": "Filter met land code.", "country": "Filter met land code.",
"debug": "Schakel debug in.", "debug": "Schakel debug in.",
"experimental": "Zet experimentele functies aan", "experimental": "Zet experimentele functies aan",
"netdaemon": "Zet NetDaemon apps ontdekken & traceren aan",
"not_in_use": "Niet in gebruik met YAML", "not_in_use": "Niet in gebruik met YAML",
"release_limit": "Aantal releases om te laten zien.", "release_limit": "Aantal releases om te laten zien.",
"sidepanel_icon": "Zijpaneel icoon", "sidepanel_icon": "Zijpaneel icoon",
@@ -84,6 +88,7 @@
}, },
"repository_banner": { "repository_banner": {
"config_flow": "Deze integratie ondersteunt config_flow, wat betekent dat u via uw \"Instellingen\" naar \"Integraties\" kunt gaan om het te configureren.", "config_flow": "Deze integratie ondersteunt config_flow, wat betekent dat u via uw \"Instellingen\" naar \"Integraties\" kunt gaan om het te configureren.",
"config_flow_title": "UI-configuratie ondersteund",
"integration_not_loaded": "Deze integratie wordt niet geladen in Home Assistant.", "integration_not_loaded": "Deze integratie wordt niet geladen in Home Assistant.",
"no_restart_required": "Geen herstart vereist", "no_restart_required": "Geen herstart vereist",
"not_loaded": "Niet geladen", "not_loaded": "Niet geladen",

86
config/custom_components/hacs/.translations/nn.json Executable file → Normal file
View File

@@ -4,14 +4,17 @@
"appdaemon": "AppDaemon", "appdaemon": "AppDaemon",
"appdaemon_apps": "AppDeamon-appar", "appdaemon_apps": "AppDeamon-appar",
"background_task": "Bakgrunnsoppgåve køyrer. Denne sida kjem til å laste seg omatt når ho er ferdig.", "background_task": "Bakgrunnsoppgåve køyrer. Denne sida kjem til å laste seg omatt når ho er ferdig.",
"continue": "Fortsette", "check_log_file": "Sjå i loggfila di for meir detaljar.",
"continue": "Hald fram",
"disabled": "Deaktivert", "disabled": "Deaktivert",
"documentation": "dokumentasjon", "documentation": "Dokumentasjon",
"hacs_is_disabled": "HACS er deaktivert", "hacs_is_disabled": "HACS er deaktivert",
"installed": "Installert", "installed": "Installert",
"integration": "Integrasjon", "integration": "Integrasjon",
"integrations": "Integrasjonar", "integrations": "Integrasjonar",
"manage": "manage", "manage": "Handtere",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDeamon-appar",
"plugin": "Tillegg", "plugin": "Tillegg",
"plugins": "Tillegg", "plugins": "Tillegg",
"python_script": "Pythonskript", "python_script": "Pythonskript",
@@ -33,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Aktiver AppDeamon-appar-oppdaging og sporing", "appdaemon": "Aktiver AppDeamon-appar-oppdaging og sporing",
"netdaemon": "Aktiver NetDeamon-appar-oppdaging og sporing",
"python_script": "Aktiver pythonscript-oppdaging og sporing", "python_script": "Aktiver pythonscript-oppdaging og sporing",
"sidepanel_icon": "Sidepanelikon", "sidepanel_icon": "Sidepanelikon",
"sidepanel_title": "Sidepaneltittel", "sidepanel_title": "Sidepaneltittel",
@@ -47,18 +51,23 @@
}, },
"confirm": { "confirm": {
"add_to_lovelace": "Er du sikker på at du vil legge til dette i Lovelace-ressursane dine?", "add_to_lovelace": "Er du sikker på at du vil legge til dette i Lovelace-ressursane dine?",
"bg_task": "Handlinga er deaktivert medan bakgrunnsoppgåveene køyrer.",
"cancel": "Avbryt", "cancel": "Avbryt",
"continue": "Er du sikker på at du vil halde fram?", "continue": "Er du sikker på at du vil halde fram?",
"delete": "Er du sikker på at du vil slette '{item}'?", "delete": "Er du sikker på at du vil slette '{item}'?",
"exist": "{item} eksisterer allerede", "delete_installed": "'{item}' er installert. Du må avinstallere det før du kan slette det.",
"exist": "{item} eksisterer allereie",
"generic": "Er du sikker?", "generic": "Er du sikker?",
"home_assistant_is_restarting": "Vent, Home Assistant starter på nytt.", "home_assistant_is_restarting": "Vent... Home Assistant starter på nytt no.",
"home_assistant_version_not_correct": "Du køyrer Home Assistant-versjonen '{haversion}', men dette kodedepoet krev minimum versjon '{minversion}' for å bli installert.",
"no": "Nei", "no": "Nei",
"no_upgrades": "Ingen ventande oppgradringer",
"ok": "OK", "ok": "OK",
"overwrite": "Ved å gjere dette kjem du til å overskrive.", "overwrite": "Ved å gjere dette kjem du til å overskrive.",
"reload_data": "Dette laster inn dataa til depota HACS veit om, og dette vil ta litt tid å fullføre.",
"restart_home_assistant": "Er du sikker på at du vil starte Home Assistant på nytt?", "restart_home_assistant": "Er du sikker på at du vil starte Home Assistant på nytt?",
"uninstall": "Er du sikker på at du vil avinstallere '{item}'?", "uninstall": "Er du sikker på at du vil avinstallere '{item}'?",
"upgrade_all": "Dette vil oppgradere alle disse repositorene, sørg for at du har lest utgivelses notatene for dem alle før du fortsetter.", "upgrade_all": "Dette kjem til å oppgradere alle depota. Ver sikker på at du har lest alle versjonsmerknadene før du held fram.",
"yes": "Ja" "yes": "Ja"
}, },
"options": { "options": {
@@ -69,6 +78,8 @@
"country": "Filterer med landskode", "country": "Filterer med landskode",
"debug": "Aktiver debug.", "debug": "Aktiver debug.",
"experimental": "Aktiver ekspreimentelle funksjonar", "experimental": "Aktiver ekspreimentelle funksjonar",
"netdaemon": "Aktiver NetDeamon-appar-oppdaging og sporing",
"not_in_use": "Kan ikkje brukast saman med YAML",
"release_limit": "Talet på utgivingar", "release_limit": "Talet på utgivingar",
"sidepanel_icon": "Sidepanelikon", "sidepanel_icon": "Sidepanelikon",
"sidepanel_title": "Sidepaneltittel" "sidepanel_title": "Sidepaneltittel"
@@ -77,8 +88,10 @@
} }
}, },
"repository_banner": { "repository_banner": {
"config_flow": "Denne integrasjonen støttar config_flow, som betyr at du no kan gå til integrasjonssida i brukargrensesnittet for å konfigurere den.",
"config_flow_title": "UI-konfigurasjon støtta",
"integration_not_loaded": "Integrasjonen er ikkje lasta inn i Home Assistant.", "integration_not_loaded": "Integrasjonen er ikkje lasta inn i Home Assistant.",
"no_restart_required": "Ingen omstart kreves", "no_restart_required": "Ingen omstart kravd",
"not_loaded": "Ikkje lasta", "not_loaded": "Ikkje lasta",
"plugin_not_loaded": "Tillegget er ikkje lagt til i Lovelace-ressursane dine.", "plugin_not_loaded": "Tillegget er ikkje lagt til i Lovelace-ressursane dine.",
"restart": "Du må starte Home Assistant på nytt", "restart": "Du må starte Home Assistant på nytt",
@@ -92,9 +105,9 @@
"changelog": "Endre logg", "changelog": "Endre logg",
"downloads": "Nedlastinger", "downloads": "Nedlastinger",
"flag_this": "Marker dette", "flag_this": "Marker dette",
"frontend_version": "Frontend versjon", "frontend_version": "Frontend-versjon",
"github_stars": "GitHub-stjerner", "github_stars": "GitHub-stjerner",
"goto_integrations": "Gå til integrasjoner", "goto_integrations": "Gå til integrasjonar",
"hide": "Gøym", "hide": "Gøym",
"hide_beta": "Gøym beta", "hide_beta": "Gøym beta",
"install": "Installer", "install": "Installer",
@@ -121,7 +134,7 @@
"add_custom_repository": "LEGG TIL EIN ANNAN REPOSITORY", "add_custom_repository": "LEGG TIL EIN ANNAN REPOSITORY",
"adding_new_repo": "Legger til ny repository '{repo}'", "adding_new_repo": "Legger til ny repository '{repo}'",
"adding_new_repo_category": "Med kategori '{category}'.", "adding_new_repo_category": "Med kategori '{category}'.",
"bg_task_custom": "Custom repositories er skjult mens bakgrunnsoppgaver kjører.", "bg_task_custom": "Custom repositories er skjult medan bakgrunnsoppgaver køyrer.",
"category": "Kategori", "category": "Kategori",
"compact_mode": "Kompaktmodus", "compact_mode": "Kompaktmodus",
"custom_repositories": "VANLEG REPOSITORY", "custom_repositories": "VANLEG REPOSITORY",
@@ -130,10 +143,11 @@
"grid": "rutenett", "grid": "rutenett",
"hacs_repo": "HACS repo", "hacs_repo": "HACS repo",
"hidden_repositories": "gøymde repositories", "hidden_repositories": "gøymde repositories",
"open_repository": "Åpne repository", "missing_category": "Du må velje éin kategori",
"open_repository": "Opne repository",
"reload_data": "Last om dataa", "reload_data": "Last om dataa",
"reload_window": "Last inn vinduet på nytt", "reload_window": "Last inn vindauget på nytt",
"repository_configuration": "Repository konfigurasjon", "repository_configuration": "Repository-konfigurasjon",
"save": "Lagre", "save": "Lagre",
"table": "Tavle", "table": "Tavle",
"table_view": "Tabellvisning", "table_view": "Tabellvisning",
@@ -141,43 +155,43 @@
"upgrade_all": "Oppdater alle" "upgrade_all": "Oppdater alle"
}, },
"store": { "store": {
"ascending": "stigende", "ascending": "stigande",
"clear_new": "Fjern alle nye repositories", "clear_new": "Fjern alle nye repositories",
"descending": "synkende", "descending": "synkande",
"last_updated": "Sist oppdatert", "last_updated": "Sist oppdatert",
"name": "Namn", "name": "Namn",
"new_repositories": "Ny repository", "new_repositories": "Ny repository",
"pending_upgrades": "Venter på oppgraderinger", "pending_upgrades": "Ventande oppgraderinger",
"placeholder_search": "Ver vennleg og skriv inn ei søkefrase", "placeholder_search": "Ver vennleg og skriv inn ei søkefrase",
"sort": "Sorter", "sort": "Sorter",
"stars": "Stjerner", "stars": "Stjerner",
"status": "Status" "status": "Status"
}, },
"time": { "time": {
"ago": "siden", "ago": "sidan",
"day": "dag", "day": "dag",
"days": "dager", "days": "dagar",
"hour": "time", "hour": "time",
"hours": "timer", "hours": "timar",
"minute": "minutt", "minute": "minutt",
"minutes": "minutter", "minutes": "minutt",
"month": "måned", "month": "månad",
"months": "måneder", "months": "månadar",
"one": "En", "one": "Éin",
"one_day_ago": "for en dag siden", "one_day_ago": "for éin dag sidan",
"one_hour_ago": "en time siden", "one_hour_ago": "éin time sidan",
"one_minute_ago": "ett minutt siden", "one_minute_ago": "eitt minutt sidan",
"one_month_ago": "en måned siden", "one_month_ago": "ein månad sidan",
"one_second_ago": "ett sekund siden", "one_second_ago": "eitt sekund sidan",
"one_year_ago": "ett år siden", "one_year_ago": "eitt år sidan",
"second": "sekund", "second": "sekund",
"seconds": "sekunder", "seconds": "sekund",
"x_days_ago": "{x} dager siden", "x_days_ago": "{x} dagar siden",
"x_hours_ago": "{x} timer siden", "x_hours_ago": "{x} timer sidan",
"x_minutes_ago": "{x} minutter siden", "x_minutes_ago": "{x} minutt sidan",
"x_months_ago": "{x} måneder siden", "x_months_ago": "{x} månadar sidan",
"x_seconds_ago": "{x} sekunder siden", "x_seconds_ago": "{x} sekund sidan",
"x_years_ago": "{x} år siden", "x_years_ago": "{x} år sidan",
"year": "år", "year": "år",
"years": "år" "years": "år"
} }

4
config/custom_components/hacs/.translations/pl.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integracja", "integration": "Integracja",
"integrations": "Integracje", "integrations": "Integracje",
"manage": "zarządzaj", "manage": "zarządzaj",
"netdaemon": "NetDaemon",
"netdaemon_apps": "Aplikacje NetDaemon",
"plugin": "Wtyczka", "plugin": "Wtyczka",
"plugins": "Wtyczki", "plugins": "Wtyczki",
"python_script": "Skrypt Python", "python_script": "Skrypt Python",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Włącz wykrywanie i śledzenie aplikacji AppDaemon", "appdaemon": "Włącz wykrywanie i śledzenie aplikacji AppDaemon",
"netdaemon": "Włącz wykrywanie i śledzenie aplikacji NetDaemon",
"python_script": "Włącz wykrywanie i śledzenie skryptów Python", "python_script": "Włącz wykrywanie i śledzenie skryptów Python",
"sidepanel_icon": "Ikona w panelu bocznym", "sidepanel_icon": "Ikona w panelu bocznym",
"sidepanel_title": "Tytuł w panelu bocznym", "sidepanel_title": "Tytuł w panelu bocznym",
@@ -75,6 +78,7 @@
"country": "Filtruj według kodu kraju", "country": "Filtruj według kodu kraju",
"debug": "Włącz debugowanie.", "debug": "Włącz debugowanie.",
"experimental": "Włącz funkcje eksperymentalne", "experimental": "Włącz funkcje eksperymentalne",
"netdaemon": "Włącz wykrywanie i śledzenie aplikacji NetDaemon",
"not_in_use": "Nieużywany z YAML", "not_in_use": "Nieużywany z YAML",
"release_limit": "Liczba wydań do wyświetlenia", "release_limit": "Liczba wydań do wyświetlenia",
"sidepanel_icon": "Ikona w panelu bocznym", "sidepanel_icon": "Ikona w panelu bocznym",

15
config/custom_components/hacs/.translations/pt-BR.json Executable file → Normal file
View File

@@ -12,6 +12,8 @@
"integration": "Integração", "integration": "Integração",
"integrations": "Integrações", "integrations": "Integrações",
"manage": "gerenciar", "manage": "gerenciar",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Apps",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Python Script", "python_script": "Python Script",
@@ -33,6 +35,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento", "appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento",
"netdaemon": "Habilitar NetDaemon apps descoberta & rastreamento",
"python_script": "Habilitar python_scripts descoberta & rastreamento", "python_script": "Habilitar python_scripts descoberta & rastreamento",
"sidepanel_icon": "Icone do painel lateral", "sidepanel_icon": "Icone do painel lateral",
"sidepanel_title": "Titulo do painel lateral", "sidepanel_title": "Titulo do painel lateral",
@@ -47,10 +50,13 @@
}, },
"confirm": { "confirm": {
"cancel": "Cancelar", "cancel": "Cancelar",
"continue": "Tem certeza que quer continuar?",
"delete": "Tem certeza de que deseja excluir '{item}'?", "delete": "Tem certeza de que deseja excluir '{item}'?",
"exist": "{item} já existe", "exist": "{item} já existe",
"home_assistant_is_restarting": "Espere, o Home Assistant está agora a reiniciar.",
"no": "Não", "no": "Não",
"ok": "OK", "ok": "OK",
"restart_home_assistant": "Tem certeza de que deseja reiniciar o Home Assistant?",
"uninstall": "Tem certeza de que deseja desinstalar '{item}'?", "uninstall": "Tem certeza de que deseja desinstalar '{item}'?",
"yes": "Sim" "yes": "Sim"
}, },
@@ -61,6 +67,8 @@
"appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento", "appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento",
"country": "Filtro pelo código do país.", "country": "Filtro pelo código do país.",
"experimental": "Ativar recursos experimentais", "experimental": "Ativar recursos experimentais",
"netdaemon": "Habilitar NetDaemon apps descoberta & rastreamento",
"not_in_use": "Não está em uso com o YAML",
"release_limit": "Número de lançamentos a serem exibidos.", "release_limit": "Número de lançamentos a serem exibidos.",
"sidepanel_icon": "Icone do painel lateral", "sidepanel_icon": "Icone do painel lateral",
"sidepanel_title": "Titulo do painel lateral" "sidepanel_title": "Titulo do painel lateral"
@@ -69,6 +77,7 @@
} }
}, },
"repository_banner": { "repository_banner": {
"integration_not_loaded": "Esta integração não é carregada no Home Assistant.",
"no_restart_required": "Não é necessário reiniciar", "no_restart_required": "Não é necessário reiniciar",
"not_loaded": "Não carregado", "not_loaded": "Não carregado",
"restart": "Você precisa reiniciar o Home Assistant.", "restart": "Você precisa reiniciar o Home Assistant.",
@@ -80,6 +89,7 @@
"available": "Disponível", "available": "Disponível",
"back_to": "Voltar para", "back_to": "Voltar para",
"changelog": "Changelog", "changelog": "Changelog",
"downloads": "Downloads",
"flag_this": "Sinalizar isso", "flag_this": "Sinalizar isso",
"frontend_version": "Versão Frontend", "frontend_version": "Versão Frontend",
"github_stars": "Estrelas de GitHub", "github_stars": "Estrelas de GitHub",
@@ -118,12 +128,14 @@
"grid": "Grade", "grid": "Grade",
"hacs_repo": "HACS repo", "hacs_repo": "HACS repo",
"hidden_repositories": "repositórios ocultos", "hidden_repositories": "repositórios ocultos",
"missing_category": "Você precisa selecionar uma categoria",
"open_repository": "Repositório aberto", "open_repository": "Repositório aberto",
"reload_data": "Recarregar dados", "reload_data": "Recarregar dados",
"reload_window": "Recarregar janela", "reload_window": "Recarregar janela",
"repository_configuration": "Configuração do Repositório", "repository_configuration": "Configuração do Repositório",
"save": "Salvar", "save": "Salvar",
"table": "Tabela", "table": "Tabela",
"table_view": "Vista de mesa",
"unhide": "reexibir", "unhide": "reexibir",
"upgrade_all": "Atualizar tudo" "upgrade_all": "Atualizar tudo"
}, },
@@ -136,7 +148,8 @@
"new_repositories": "Novos Repositórios", "new_repositories": "Novos Repositórios",
"pending_upgrades": "Atualizações pendentes", "pending_upgrades": "Atualizações pendentes",
"placeholder_search": "Por favor insira um termo de pesquisa...", "placeholder_search": "Por favor insira um termo de pesquisa...",
"stars": "Estrelas" "stars": "Estrelas",
"status": "Status"
}, },
"time": { "time": {
"ago": "atrás", "ago": "atrás",

17
config/custom_components/hacs/.translations/ro.json Executable file → Normal file
View File

@@ -11,6 +11,8 @@
"installed": "instalat", "installed": "instalat",
"integration": "Integrare", "integration": "Integrare",
"integrations": "Integrări", "integrations": "Integrări",
"netdaemon": "NetDaemon",
"netdaemon_apps": "Aplicații NetDaemon",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugin-uri", "plugins": "Plugin-uri",
"python_script": "Script Python", "python_script": "Script Python",
@@ -32,6 +34,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Activați descoperirea și urmărirea aplicațiilor AppDaemon", "appdaemon": "Activați descoperirea și urmărirea aplicațiilor AppDaemon",
"netdaemon": "Activați descoperirea și urmărirea aplicațiilor NetDaemon",
"python_script": "Activați descoperirea și urmărirea python_scripts", "python_script": "Activați descoperirea și urmărirea python_scripts",
"sidepanel_icon": "Pictogramă Panou lateral", "sidepanel_icon": "Pictogramă Panou lateral",
"sidepanel_title": "Titlu panou lateral", "sidepanel_title": "Titlu panou lateral",
@@ -45,6 +48,7 @@
"title": "HACS (Home Assistant Community Store)" "title": "HACS (Home Assistant Community Store)"
}, },
"confirm": { "confirm": {
"bg_task": "Acțiunea este dezactivată în timp ce activitățile de fundal se execută.",
"cancel": "Anulare", "cancel": "Anulare",
"delete": "Sigur doriți să ștergeți '{item}'?", "delete": "Sigur doriți să ștergeți '{item}'?",
"exist": "{item} există deja", "exist": "{item} există deja",
@@ -60,6 +64,7 @@
"appdaemon": "Activați descoperirea și urmărirea aplicațiilor AppDaemon", "appdaemon": "Activați descoperirea și urmărirea aplicațiilor AppDaemon",
"country": "Filtrează cu codul țării.", "country": "Filtrează cu codul țării.",
"experimental": "Activați funcțiile experimentale", "experimental": "Activați funcțiile experimentale",
"netdaemon": "Activați descoperirea și urmărirea aplicațiilor NetDaemon",
"not_in_use": "Nu este utilizat cu YAML", "not_in_use": "Nu este utilizat cu YAML",
"release_limit": "Număr de versiuni afișate.", "release_limit": "Număr de versiuni afișate.",
"sidepanel_icon": "Pictogramă Panou lateral", "sidepanel_icon": "Pictogramă Panou lateral",
@@ -69,8 +74,10 @@
} }
}, },
"repository_banner": { "repository_banner": {
"integration_not_loaded": "Această integrare nu este încărcată în Home Assistant.",
"no_restart_required": "Nu este necesară repornirea", "no_restart_required": "Nu este necesară repornirea",
"restart": "Trebuie să reporniți Home Assistant." "restart": "Trebuie să reporniți Home Assistant.",
"restart_pending": "Reporniți în așteptare"
}, },
"repository": { "repository": {
"add_to_lovelace": "Adăugați la Lovelace", "add_to_lovelace": "Adăugați la Lovelace",
@@ -81,6 +88,7 @@
"downloads": "Descărcări", "downloads": "Descărcări",
"flag_this": "Semnalizează", "flag_this": "Semnalizează",
"frontend_version": "Versiune frontend", "frontend_version": "Versiune frontend",
"github_stars": "Stele GitHub",
"hide": "Ascunde", "hide": "Ascunde",
"hide_beta": "Ascundere beta", "hide_beta": "Ascundere beta",
"install": "Instalează", "install": "Instalează",
@@ -97,6 +105,7 @@
"open_plugin": "Deschide plugin", "open_plugin": "Deschide plugin",
"reinstall": "Reinstalare", "reinstall": "Reinstalare",
"repository": "Depozit", "repository": "Depozit",
"restart_home_assistant": "Reporniți Home Assistant",
"show_beta": "Afișare beta", "show_beta": "Afișare beta",
"uninstall": "Dezinstalare", "uninstall": "Dezinstalare",
"update_information": "Actualizare informație", "update_information": "Actualizare informație",
@@ -114,8 +123,11 @@
"grid": "Grilă", "grid": "Grilă",
"hacs_repo": "HACS repo", "hacs_repo": "HACS repo",
"hidden_repositories": "depozite ascunse", "hidden_repositories": "depozite ascunse",
"missing_category": "Trebuie să selectați o categorie",
"open_repository": "Deschideți depozitul",
"reload_data": "Reîncărcați datele", "reload_data": "Reîncărcați datele",
"reload_window": "Reîncărcați fereastra", "reload_window": "Reîncărcați fereastra",
"repository_configuration": "Configurația depozitului",
"save": "Salveaza", "save": "Salveaza",
"table": "Tabel", "table": "Tabel",
"table_view": "Vizualizare tabel", "table_view": "Vizualizare tabel",
@@ -130,7 +142,8 @@
"new_repositories": "Noi depozite", "new_repositories": "Noi depozite",
"placeholder_search": "Vă rugăm să introduceți un termen de căutare ...", "placeholder_search": "Vă rugăm să introduceți un termen de căutare ...",
"sort": "fel", "sort": "fel",
"stars": "Stele" "stars": "Stele",
"status": "Starea"
}, },
"time": { "time": {
"ago": "în urmă", "ago": "în urmă",

8
config/custom_components/hacs/.translations/ru.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Интеграция", "integration": "Интеграция",
"integrations": "Интеграции", "integrations": "Интеграции",
"manage": "управлять", "manage": "управлять",
"netdaemon": "NetDaemon",
"netdaemon_apps": "Приложения NetDaemon",
"plugin": "Плагин", "plugin": "Плагин",
"plugins": "Плагины", "plugins": "Плагины",
"python_script": "Скрипт Python", "python_script": "Скрипт Python",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Включить поиск и отслеживание приложений AppDaemon", "appdaemon": "Включить поиск и отслеживание приложений AppDaemon",
"netdaemon": "Включить поиск и отслеживание приложений NetDaemon",
"python_script": "Включить поиск и отслеживание скриптов Python", "python_script": "Включить поиск и отслеживание скриптов Python",
"sidepanel_icon": "Иконка в боковом меню", "sidepanel_icon": "Иконка в боковом меню",
"sidepanel_title": "Название в боковом меню", "sidepanel_title": "Название в боковом меню",
@@ -69,9 +72,10 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Включить поиск и отслеживание приложений AppDaemon", "appdaemon": "Включить поиск и отслеживание приложений AppDaemon",
"country": "Фильтр с кодом страны.", "country": "Фильтр по стране.",
"debug": "Включить отладку.", "debug": "Включить отладку.",
"experimental": "Включить экспериментальные функции", "experimental": "Вкл. экспериментальные функции",
"netdaemon": "Включить поиск и отслеживание приложений NetDaemon",
"not_in_use": "Не используется с YAML", "not_in_use": "Не используется с YAML",
"release_limit": "Число доступных версий.", "release_limit": "Число доступных версий.",
"sidepanel_icon": "Иконка в боковом меню", "sidepanel_icon": "Иконка в боковом меню",

6
config/custom_components/hacs/.translations/sl.json Executable file → Normal file
View File

@@ -13,6 +13,8 @@
"integration": "Integracija", "integration": "Integracija",
"integrations": "Integracije", "integrations": "Integracije",
"manage": "upravljanje", "manage": "upravljanje",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Aplikacije",
"plugin": "vtičnik", "plugin": "vtičnik",
"plugins": "Vtičniki", "plugins": "Vtičniki",
"python_script": "Python skripta", "python_script": "Python skripta",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Omogoči odkrivanje in sledenje aplikacij AppDaemon", "appdaemon": "Omogoči odkrivanje in sledenje aplikacij AppDaemon",
"netdaemon": "Omogoči odkrivanje in sledenje aplikacij NetDaemon",
"python_script": "Omogoči odkrivanje in sledenje python_scripts", "python_script": "Omogoči odkrivanje in sledenje python_scripts",
"sidepanel_icon": "Ikona stranske plošče", "sidepanel_icon": "Ikona stranske plošče",
"sidepanel_title": "Naslov stranske plošče", "sidepanel_title": "Naslov stranske plošče",
@@ -56,6 +59,7 @@
"exist": "{item} že obstaja", "exist": "{item} že obstaja",
"generic": "Ali si prepričan?", "generic": "Ali si prepričan?",
"home_assistant_is_restarting": "Počakaj, Home Assistant se zdaj znova zaganja.", "home_assistant_is_restarting": "Počakaj, Home Assistant se zdaj znova zaganja.",
"home_assistant_version_not_correct": "Uporabljate Home Assistant verzije '{haversion}', vendar to skladišče zahteva nameščeno najmanj različico '{minversion}'.",
"no": "Ne", "no": "Ne",
"no_upgrades": "Ni nadgradenj", "no_upgrades": "Ni nadgradenj",
"ok": "V redu", "ok": "V redu",
@@ -74,6 +78,7 @@
"country": "Filtrirajte s kodo države.", "country": "Filtrirajte s kodo države.",
"debug": "Omogoči odpravljanje napak.", "debug": "Omogoči odpravljanje napak.",
"experimental": "Omogočite poskusne funkcije", "experimental": "Omogočite poskusne funkcije",
"netdaemon": "Omogoči odkrivanje in sledenje aplikacij NetDaemon",
"not_in_use": "Ni v uporabi z YAML", "not_in_use": "Ni v uporabi z YAML",
"release_limit": "Število izdaj, ki jih želite prikazati.", "release_limit": "Število izdaj, ki jih želite prikazati.",
"sidepanel_icon": "Ikona stranske plošče", "sidepanel_icon": "Ikona stranske plošče",
@@ -84,6 +89,7 @@
}, },
"repository_banner": { "repository_banner": {
"config_flow": "Ta integracija podpira config_flow, kar pomeni, da lahko zdaj greste na odsek integracije vašega uporabniškega vmesnika, da ga konfigurirate.", "config_flow": "Ta integracija podpira config_flow, kar pomeni, da lahko zdaj greste na odsek integracije vašega uporabniškega vmesnika, da ga konfigurirate.",
"config_flow_title": "Konfiguracija uporabniškega vmesnika je podprta",
"integration_not_loaded": "Ta integracija ni naložena v programu Home Assistant.", "integration_not_loaded": "Ta integracija ni naložena v programu Home Assistant.",
"no_restart_required": "Ponovni zagon ni potreben", "no_restart_required": "Ponovni zagon ni potreben",
"not_loaded": "Ni naloženo", "not_loaded": "Ni naloženo",

7
config/custom_components/hacs/.translations/sv.json Executable file → Normal file
View File

@@ -11,6 +11,8 @@
"installed": "installerad", "installed": "installerad",
"integration": "Integration", "integration": "Integration",
"integrations": "Integrationer", "integrations": "Integrationer",
"netdaemon": "NetDaemon",
"netdaemon_apps": "NetDaemon Applikationer",
"plugin": "Plugin", "plugin": "Plugin",
"plugins": "Plugins", "plugins": "Plugins",
"python_script": "Python skript", "python_script": "Python skript",
@@ -32,6 +34,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "Upptäck och följ Appdaemon applikationer", "appdaemon": "Upptäck och följ Appdaemon applikationer",
"netdaemon": "Upptäck och följ NetDaemon applikationer",
"python_script": "Upptäck och följ python_scripts", "python_script": "Upptäck och följ python_scripts",
"sidepanel_icon": "Ikon för sidpanel", "sidepanel_icon": "Ikon för sidpanel",
"sidepanel_title": "Rubrik för sidpanel", "sidepanel_title": "Rubrik för sidpanel",
@@ -70,6 +73,7 @@
"country": "Filtrera på landskod.", "country": "Filtrera på landskod.",
"debug": "Aktivera felsökning", "debug": "Aktivera felsökning",
"experimental": "Använd experimentella funktioner", "experimental": "Använd experimentella funktioner",
"netdaemon": "Upptäck och följ NetDaemon applikationer",
"release_limit": "Antalet releaser som visas.", "release_limit": "Antalet releaser som visas.",
"sidepanel_icon": "Ikon för sidpanel", "sidepanel_icon": "Ikon för sidpanel",
"sidepanel_title": "Rubrik för sidpanel" "sidepanel_title": "Rubrik för sidpanel"
@@ -112,6 +116,7 @@
"open_plugin": "Öppna plugin", "open_plugin": "Öppna plugin",
"reinstall": "Ominstallera", "reinstall": "Ominstallera",
"repository": "Repository", "repository": "Repository",
"restart_home_assistant": "Starta om Home Assistant",
"show_beta": "Visa betaversioner", "show_beta": "Visa betaversioner",
"uninstall": "Avinstallera", "uninstall": "Avinstallera",
"update_information": "Uppdatera information", "update_information": "Uppdatera information",
@@ -131,6 +136,7 @@
"hacs_repo": "HACS repo", "hacs_repo": "HACS repo",
"hidden_repositories": "dolda förråd", "hidden_repositories": "dolda förråd",
"missing_category": "Du behöver välja en kategori", "missing_category": "Du behöver välja en kategori",
"open_repository": "Öppna Repository",
"reload_data": "Ladda om data", "reload_data": "Ladda om data",
"reload_window": "Ladda om fönstret", "reload_window": "Ladda om fönstret",
"save": "Spara", "save": "Spara",
@@ -167,6 +173,7 @@
"one_minute_ago": "en minut sedan", "one_minute_ago": "en minut sedan",
"one_month_ago": "en månad sedan", "one_month_ago": "en månad sedan",
"one_second_ago": "för en sekund sedan", "one_second_ago": "för en sekund sedan",
"one_year_ago": "ett år sedan",
"second": "andra", "second": "andra",
"seconds": "sekunder", "seconds": "sekunder",
"x_days_ago": "{x} dagar sedan", "x_days_ago": "{x} dagar sedan",

View File

@@ -13,6 +13,8 @@
"integration": "自定义组件", "integration": "自定义组件",
"integrations": "自定义组件", "integrations": "自定义组件",
"manage": "管理", "manage": "管理",
"netdaemon": "NetDaemon应用",
"netdaemon_apps": "NetDaemon应用",
"plugin": "Lovelace插件", "plugin": "Lovelace插件",
"plugins": "Lovelace插件", "plugins": "Lovelace插件",
"python_script": "Python脚本", "python_script": "Python脚本",
@@ -34,6 +36,7 @@
"user": { "user": {
"data": { "data": {
"appdaemon": "启用AppDaemon应用发现和跟踪", "appdaemon": "启用AppDaemon应用发现和跟踪",
"netdaemon": "启用NetDaemon应用发现和跟踪",
"python_script": "启用python_scripts发现和跟踪", "python_script": "启用python_scripts发现和跟踪",
"sidepanel_icon": "侧面板图标", "sidepanel_icon": "侧面板图标",
"sidepanel_title": "侧面板标题", "sidepanel_title": "侧面板标题",
@@ -52,12 +55,12 @@
"cancel": "取消", "cancel": "取消",
"continue": "你确定你要继续吗?", "continue": "你确定你要继续吗?",
"delete": "是否确实要删除\"{item}\"", "delete": "是否确实要删除\"{item}\"",
"delete_installed": "已安装 {item},需要先将其卸载,然后才能将其删除。", "delete_installed": "已安装 '{item}',需要先将其卸载,然后才能将其删除。",
"exist": "{item}已经存在", "exist": "{item}已经存在",
"generic": "你确定吗?", "generic": "你确定吗?",
"home_assistant_is_restarting": "请等待Home Assistant现在正在重新启动。", "home_assistant_is_restarting": "请等待Home Assistant现在正在重新启动。",
"home_assistant_version_not_correct": "您正在运行Home Assistant的版本为'{haversion}',但是这个需要安装最低版本是'{minversion}'。", "home_assistant_version_not_correct": "您正在运行Home Assistant的版本为'{haversion}',但是这个需要安装最低版本是'{minversion}'。",
"no": "", "no": "取消",
"no_upgrades": "暂无升级", "no_upgrades": "暂无升级",
"ok": "确定", "ok": "确定",
"overwrite": "这样做会覆盖它。", "overwrite": "这样做会覆盖它。",
@@ -65,7 +68,7 @@
"restart_home_assistant": "您确定要重新启动Home Assistant吗", "restart_home_assistant": "您确定要重新启动Home Assistant吗",
"uninstall": "您确定要卸载“ {item} ”吗?", "uninstall": "您确定要卸载“ {item} ”吗?",
"upgrade_all": "这将升级所有这些仓库,请确保在继续之前已阅读所有仓库的发行说明。", "upgrade_all": "这将升级所有这些仓库,请确保在继续之前已阅读所有仓库的发行说明。",
"yes": "" "yes": "确定"
}, },
"options": { "options": {
"step": { "step": {
@@ -75,6 +78,7 @@
"country": "用国家代码过滤。", "country": "用国家代码过滤。",
"debug": "启用调试。", "debug": "启用调试。",
"experimental": "启用实验功能", "experimental": "启用实验功能",
"netdaemon": "启用NetDaemon应用发现和跟踪",
"not_in_use": "不适用于 YAML", "not_in_use": "不适用于 YAML",
"release_limit": "要显示的发行数量。", "release_limit": "要显示的发行数量。",
"sidepanel_icon": "侧面板图标", "sidepanel_icon": "侧面板图标",
@@ -128,8 +132,8 @@
}, },
"settings": { "settings": {
"add_custom_repository": "添加自定义仓库", "add_custom_repository": "添加自定义仓库",
"adding_new_repo": "添加新的仓库 {repo}", "adding_new_repo": "添加新的仓库 '{repo}'",
"adding_new_repo_category": "类别为 {category}。", "adding_new_repo_category": "类别为 '{category}'。",
"bg_task_custom": "自定义仓库在后台任务运行时被隐藏。", "bg_task_custom": "自定义仓库在后台任务运行时被隐藏。",
"category": "类别", "category": "类别",
"compact_mode": "紧凑模式", "compact_mode": "紧凑模式",

105
config/custom_components/hacs/__init__.py Executable file → Normal file
View File

@@ -4,6 +4,7 @@ Custom element manager for community created elements.
For more details about this integration, please refer to the documentation at For more details about this integration, please refer to the documentation at
https://hacs.xyz/ https://hacs.xyz/
""" """
import voluptuous as vol import voluptuous as vol
from aiogithubapi import AIOGitHub from aiogithubapi import AIOGitHub
from homeassistant import config_entries from homeassistant import config_entries
@@ -14,13 +15,23 @@ from homeassistant.exceptions import ConfigEntryNotReady, ServiceNotFound
from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema from custom_components.hacs.configuration_schema import (
from .const import DOMAIN, ELEMENT_TYPES, STARTUP, VERSION hacs_base_config_schema,
from .constrains import check_constans hacs_config_option_schema,
from .hacsbase import Hacs )
from .hacsbase.configuration import Configuration from custom_components.hacs.const import DOMAIN, ELEMENT_TYPES, STARTUP, VERSION
from .hacsbase.data import HacsData from custom_components.hacs.constrains import check_constans, check_requirements
from .setup import add_sensor, load_hacs_repository, setup_frontend from custom_components.hacs.hacsbase.configuration import Configuration
from custom_components.hacs.hacsbase.data import HacsData
from custom_components.hacs.setup import (
add_sensor,
load_hacs_repository,
setup_frontend,
)
from custom_components.hacs.globals import get_hacs
from custom_components.hacs.helpers.network import internet_connectivity_check
SCHEMA = hacs_base_config_schema() SCHEMA = hacs_base_config_schema()
SCHEMA[vol.Optional("options")] = hacs_config_option_schema() SCHEMA[vol.Optional("options")] = hacs_config_option_schema()
@@ -29,16 +40,18 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: SCHEMA}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up this integration using yaml.""" """Set up this integration using yaml."""
hacs = get_hacs()
if DOMAIN not in config: if DOMAIN not in config:
return True return True
hass.data[DOMAIN] = config hass.data[DOMAIN] = config
Hacs.hass = hass hacs.hass = hass
Hacs.configuration = Configuration.from_dict( hacs.session = async_create_clientsession(hass)
hacs.configuration = Configuration.from_dict(
config[DOMAIN], config[DOMAIN].get("options") config[DOMAIN], config[DOMAIN].get("options")
) )
Hacs.configuration.config = config hacs.configuration.config = config
Hacs.configuration.config_type = "yaml" hacs.configuration.config_type = "yaml"
await startup_wrapper_for_yaml(Hacs) await startup_wrapper_for_yaml()
hass.async_create_task( hass.async_create_task(
hass.config_entries.flow.async_init( hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={}
@@ -49,6 +62,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, config_entry): async def async_setup_entry(hass, config_entry):
"""Set up this integration using UI.""" """Set up this integration using UI."""
hacs = get_hacs()
conf = hass.data.get(DOMAIN) conf = hass.data.get(DOMAIN)
if config_entry.source == config_entries.SOURCE_IMPORT: if config_entry.source == config_entries.SOURCE_IMPORT:
if conf is None: if conf is None:
@@ -56,25 +70,26 @@ async def async_setup_entry(hass, config_entry):
hass.config_entries.async_remove(config_entry.entry_id) hass.config_entries.async_remove(config_entry.entry_id)
) )
return False return False
Hacs.hass = hass hacs.hass = hass
hacs.session = async_create_clientsession(hass)
Hacs.configuration = Configuration.from_dict( hacs.configuration = Configuration.from_dict(
config_entry.data, config_entry.options config_entry.data, config_entry.options
) )
Hacs.configuration.config_type = "flow" hacs.configuration.config_type = "flow"
Hacs.configuration.config_entry = config_entry hacs.configuration.config_entry = config_entry
config_entry.add_update_listener(reload_hacs) config_entry.add_update_listener(reload_hacs)
startup_result = await hacs_startup(Hacs) startup_result = await hacs_startup()
if not startup_result: if not startup_result:
Hacs.system.disabled = True hacs.system.disabled = True
raise ConfigEntryNotReady raise ConfigEntryNotReady
Hacs.system.disabled = False hacs.system.disabled = False
return startup_result return startup_result
async def startup_wrapper_for_yaml(hacs): async def startup_wrapper_for_yaml():
"""Startup wrapper for yaml config.""" """Startup wrapper for yaml config."""
startup_result = await hacs_startup(hacs) hacs = get_hacs()
startup_result = await hacs_startup()
if not startup_result: if not startup_result:
hacs.system.disabled = True hacs.system.disabled = True
hacs.hass.components.frontend.async_remove_panel( hacs.hass.components.frontend.async_remove_panel(
@@ -83,13 +98,16 @@ async def startup_wrapper_for_yaml(hacs):
.replace("-", "_") .replace("-", "_")
) )
hacs.logger.info("Could not setup HACS, trying again in 15 min") hacs.logger.info("Could not setup HACS, trying again in 15 min")
async_call_later(hacs.hass, 900, startup_wrapper_for_yaml(hacs)) async_call_later(hacs.hass, 900, startup_wrapper_for_yaml())
return return
hacs.system.disabled = False hacs.system.disabled = False
async def hacs_startup(hacs): async def hacs_startup():
"""HACS startup tasks.""" """HACS startup tasks."""
hacs = get_hacs()
if not check_requirements():
return False
if hacs.configuration.debug: if hacs.configuration.debug:
try: try:
await hacs.hass.services.async_call( await hacs.hass.services.async_call(
@@ -115,20 +133,21 @@ async def hacs_startup(hacs):
hacs.data = HacsData() hacs.data = HacsData()
# Check HACS Constrains # Check HACS Constrains
if not await hacs.hass.async_add_executor_job(check_constans, hacs): if not await hacs.hass.async_add_executor_job(check_constans):
if hacs.configuration.config_type == "flow": if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None: if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry) await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False return False
# Set up frontend # Set up frontend
await setup_frontend(hacs) await setup_frontend()
# Set up sensor if not await hacs.hass.async_add_executor_job(internet_connectivity_check):
await hacs.hass.async_add_executor_job(add_sensor, hacs) hacs.logger.critical("No network connectivity")
return False
# Load HACS # Load HACS
if not await load_hacs_repository(hacs): if not await load_hacs_repository():
if hacs.configuration.config_type == "flow": if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None: if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry) await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
@@ -136,7 +155,7 @@ async def hacs_startup(hacs):
# Restore from storefiles # Restore from storefiles
if not await hacs.data.restore(): if not await hacs.data.restore():
hacs_repo = hacs().get_by_name("hacs/integration") hacs_repo = hacs.get_by_name("hacs/integration")
hacs_repo.pending_restart = True hacs_repo.pending_restart = True
if hacs.configuration.config_type == "flow": if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None: if hacs.configuration.config_entry is not None:
@@ -147,6 +166,8 @@ async def hacs_startup(hacs):
hacs.common.categories = ELEMENT_TYPES hacs.common.categories = ELEMENT_TYPES
if hacs.configuration.appdaemon: if hacs.configuration.appdaemon:
hacs.common.categories.append("appdaemon") hacs.common.categories.append("appdaemon")
if hacs.configuration.netdaemon:
hacs.common.categories.append("netdaemon")
if hacs.configuration.python_script: if hacs.configuration.python_script:
hacs.configuration.python_script = False hacs.configuration.python_script = False
if hacs.configuration.config_type == "yaml": if hacs.configuration.config_type == "yaml":
@@ -162,37 +183,39 @@ async def hacs_startup(hacs):
# Setup startup tasks # Setup startup tasks
if hacs.configuration.config_type == "yaml": if hacs.configuration.config_type == "yaml":
hacs.hass.bus.async_listen_once( hacs.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, hacs.startup_tasks())
EVENT_HOMEASSISTANT_START, hacs().startup_tasks()
)
else: else:
async_call_later(hacs.hass, 5, hacs().startup_tasks()) async_call_later(hacs.hass, 5, hacs.startup_tasks())
# Show the configuration # Show the configuration
hacs.configuration.print() hacs.configuration.print()
# Set up sensor
await hacs.hass.async_add_executor_job(add_sensor)
# Mischief managed! # Mischief managed!
return True return True
async def async_remove_entry(hass, config_entry): async def async_remove_entry(hass, config_entry):
"""Handle removal of an entry.""" """Handle removal of an entry."""
Hacs().logger.info("Disabling HACS") hacs = get_hacs()
Hacs().logger.info("Removing recuring tasks") hacs.logger.info("Disabling HACS")
for task in Hacs().recuring_tasks: hacs.logger.info("Removing recuring tasks")
for task in hacs.recuring_tasks:
task() task()
Hacs().logger.info("Removing sensor") hacs.logger.info("Removing sensor")
try: try:
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
except ValueError: except ValueError:
pass pass
Hacs().logger.info("Removing sidepanel") hacs.logger.info("Removing sidepanel")
try: try:
hass.components.frontend.async_remove_panel("hacs") hass.components.frontend.async_remove_panel("hacs")
except AttributeError: except AttributeError:
pass pass
Hacs().system.disabled = True hacs.system.disabled = True
Hacs().logger.info("HACS is now disabled") hacs.logger.info("HACS is now disabled")
async def reload_hacs(hass, config_entry): async def reload_hacs(hass, config_entry):

13
config/custom_components/hacs/config_flow.py Executable file → Normal file
View File

@@ -2,15 +2,16 @@
# pylint: disable=dangerous-default-value # pylint: disable=dangerous-default-value
import logging import logging
import voluptuous as vol import voluptuous as vol
from aiogithubapi import AIOGitHub, AIOGitHubException, AIOGitHubAuthentication from aiogithubapi import AIOGitHubException, AIOGitHubAuthentication
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import DOMAIN from .const import DOMAIN
from . import Hacs
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
from custom_components.hacs.globals import get_hacs
from custom_components.hacs.helpers.information import get_repository
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -46,7 +47,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Show the configuration form to edit location data.""" """Show the configuration form to edit location data."""
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=vol.Schema(hacs_base_config_schema(user_input)), data_schema=vol.Schema(hacs_base_config_schema(user_input, True)),
errors=self._errors, errors=self._errors,
) )
@@ -69,8 +70,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Return true if token is valid.""" """Return true if token is valid."""
try: try:
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
client = AIOGitHub(token, session) await get_repository(session, token, "hacs/org")
await client.get_repo("hacs/org")
return True return True
except (AIOGitHubException, AIOGitHubAuthentication) as exception: except (AIOGitHubException, AIOGitHubAuthentication) as exception:
_LOGGER.error(exception) _LOGGER.error(exception)
@@ -90,10 +90,11 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
hacs = get_hacs()
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)
if Hacs.configuration.config_type == "yaml": if hacs.configuration.config_type == "yaml":
schema = {vol.Optional("not_in_use", default=""): str} schema = {vol.Optional("not_in_use", default=""): str}
else: else:
schema = hacs_config_option_schema(self.config_entry.options) schema = hacs_config_option_schema(self.config_entry.options)

13
config/custom_components/hacs/configuration_schema.py Executable file → Normal file
View File

@@ -8,6 +8,7 @@ TOKEN = "token"
SIDEPANEL_TITLE = "sidepanel_title" SIDEPANEL_TITLE = "sidepanel_title"
SIDEPANEL_ICON = "sidepanel_icon" SIDEPANEL_ICON = "sidepanel_icon"
APPDAEMON = "appdaemon" APPDAEMON = "appdaemon"
NETDAEMON = "netdaemon"
PYTHON_SCRIPT = "python_script" PYTHON_SCRIPT = "python_script"
THEME = "theme" THEME = "theme"
@@ -18,7 +19,7 @@ RELEASE_LIMIT = "release_limit"
EXPERIMENTAL = "experimental" EXPERIMENTAL = "experimental"
def hacs_base_config_schema(config: dict = {}) -> dict: def hacs_base_config_schema(config: dict = {}, config_flow: bool = False) -> dict:
"""Return a shcema configuration dict for HACS.""" """Return a shcema configuration dict for HACS."""
if not config: if not config:
config = { config = {
@@ -26,14 +27,24 @@ def hacs_base_config_schema(config: dict = {}) -> dict:
SIDEPANEL_ICON: "mdi:alpha-c-box", SIDEPANEL_ICON: "mdi:alpha-c-box",
SIDEPANEL_TITLE: "HACS", SIDEPANEL_TITLE: "HACS",
APPDAEMON: False, APPDAEMON: False,
NETDAEMON: False,
PYTHON_SCRIPT: False, PYTHON_SCRIPT: False,
THEME: False, THEME: False,
} }
if config_flow:
return {
vol.Required(TOKEN, default=config.get(TOKEN)): str,
vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str,
vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str,
vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool,
vol.Optional(NETDAEMON, default=config.get(NETDAEMON)): bool,
}
return { return {
vol.Required(TOKEN, default=config.get(TOKEN)): str, vol.Required(TOKEN, default=config.get(TOKEN)): str,
vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str, vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str,
vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str, vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str,
vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool, vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool,
vol.Optional(NETDAEMON, default=config.get(NETDAEMON)): bool,
vol.Optional(PYTHON_SCRIPT, default=config.get(PYTHON_SCRIPT)): bool, vol.Optional(PYTHON_SCRIPT, default=config.get(PYTHON_SCRIPT)): bool,
vol.Optional(THEME, default=config.get(THEME)): bool, vol.Optional(THEME, default=config.get(THEME)): bool,
} }

2
config/custom_components/hacs/const.py Executable file → Normal file
View File

@@ -1,7 +1,7 @@
"""Constants for HACS""" """Constants for HACS"""
NAME_LONG = "HACS (Home Assistant Community Store)" NAME_LONG = "HACS (Home Assistant Community Store)"
NAME_SHORT = "HACS" NAME_SHORT = "HACS"
VERSION = "0.21.5" VERSION = "0.23.2"
DOMAIN = "hacs" DOMAIN = "hacs"
PROJECT_URL = "https://github.com/hacs/integration/" PROJECT_URL = "https://github.com/hacs/integration/"
CUSTOM_UPDATER_LOCATIONS = [ CUSTOM_UPDATER_LOCATIONS = [

62
config/custom_components/hacs/constrains.py Executable file → Normal file
View File

@@ -5,22 +5,25 @@ import os
from .const import CUSTOM_UPDATER_LOCATIONS, CUSTOM_UPDATER_WARNING from .const import CUSTOM_UPDATER_LOCATIONS, CUSTOM_UPDATER_WARNING
from .helpers.misc import version_left_higher_then_right from .helpers.misc import version_left_higher_then_right
from custom_components.hacs.globals import get_hacs
MINIMUM_HA_VERSION = "0.98.0" MINIMUM_HA_VERSION = "0.98.0"
def check_constans(hacs): def check_constans():
"""Check HACS constrains.""" """Check HACS constrains."""
if not constrain_translations(hacs): if not constrain_translations():
return False return False
if not constrain_custom_updater(hacs): if not constrain_custom_updater():
return False return False
if not constrain_version(hacs): if not constrain_version():
return False return False
return True return True
def constrain_custom_updater(hacs): def constrain_custom_updater():
"""Check if custom_updater exist.""" """Check if custom_updater exist."""
hacs = get_hacs()
for location in CUSTOM_UPDATER_LOCATIONS: for location in CUSTOM_UPDATER_LOCATIONS:
if os.path.exists(location.format(hacs.system.config_path)): if os.path.exists(location.format(hacs.system.config_path)):
msg = CUSTOM_UPDATER_WARNING.format( msg = CUSTOM_UPDATER_WARNING.format(
@@ -31,8 +34,9 @@ def constrain_custom_updater(hacs):
return True return True
def constrain_version(hacs): def constrain_version():
"""Check if the version is valid.""" """Check if the version is valid."""
hacs = get_hacs()
if not version_left_higher_then_right(hacs.system.ha_version, MINIMUM_HA_VERSION): if not version_left_higher_then_right(hacs.system.ha_version, MINIMUM_HA_VERSION):
hacs.logger.critical( hacs.logger.critical(
f"You need HA version {MINIMUM_HA_VERSION} or newer to use this integration." f"You need HA version {MINIMUM_HA_VERSION} or newer to use this integration."
@@ -41,11 +45,55 @@ def constrain_version(hacs):
return True return True
def constrain_translations(hacs): def constrain_translations():
"""Check if traslations exist.""" """Check if traslations exist."""
hacs = get_hacs()
if not os.path.exists( if not os.path.exists(
f"{hacs.system.config_path}/custom_components/hacs/.translations" f"{hacs.system.config_path}/custom_components/hacs/.translations"
): ):
hacs.logger.critical("You are missing the translations directory.") hacs.logger.critical("You are missing the translations directory.")
return False return False
return True return True
def check_requirements():
"""Check the requirements"""
missing = []
try:
from aiogithubapi import AIOGitHubException # pylint: disable=unused-import
except ImportError:
missing.append("aiogithubapi")
try:
from hacs_frontend import locate_gz # pylint: disable=unused-import
except ImportError:
missing.append("hacs_frontend")
try:
import semantic_version # pylint: disable=unused-import
except ImportError:
missing.append("semantic_version")
try:
from integrationhelper import Logger # pylint: disable=unused-import
except ImportError:
missing.append("integrationhelper")
try:
import backoff # pylint: disable=unused-import
except ImportError:
missing.append("backoff")
try:
import aiofiles # pylint: disable=unused-import
except ImportError:
missing.append("aiofiles")
if missing:
hacs = get_hacs()
for requirement in missing:
hacs.logger.critical(
f"Required python requirement '{requirement}' is not installed"
)
return False
return True

View File

@@ -0,0 +1,29 @@
# pylint: disable=invalid-name, missing-docstring
hacs = []
removed_repositories = []
def get_hacs():
if not hacs:
from custom_components.hacs.hacsbase import Hacs
hacs.append(Hacs())
return hacs[0]
def is_removed(repository):
return repository in [x.repository for x in removed_repositories]
def get_removed(repository):
if not is_removed(repository):
from custom_components.hacs.repositories.removed import RemovedRepository
removed_repo = RemovedRepository()
removed_repo.repository = repository
removed_repositories.append(removed_repo)
filter_repos = [
x for x in removed_repositories if x.repository.lower() == repository.lower()
]
return filter_repos[0]

127
config/custom_components/hacs/hacsbase/__init__.py Executable file → Normal file
View File

@@ -9,13 +9,20 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit
from integrationhelper import Logger from integrationhelper import Logger
from .task_factory import HacsTaskFactory from custom_components.hacs.hacsbase.task_factory import HacsTaskFactory
from .exceptions import HacsException from custom_components.hacs.hacsbase.exceptions import HacsException
from ..const import ELEMENT_TYPES from custom_components.hacs.const import ELEMENT_TYPES
from ..setup import setup_extra_stores from custom_components.hacs.setup import setup_extra_stores
from ..store import async_load_from_store, async_save_to_store from custom_components.hacs.store import async_load_from_store, async_save_to_store
from ..helpers.get_defaults import get_default_repos_lists, get_default_repos_orgs from custom_components.hacs.helpers.get_defaults import (
get_default_repos_lists,
get_default_repos_orgs,
)
from custom_components.hacs.helpers.register_repository import register_repository
from custom_components.hacs.globals import removed_repositories, get_removed, is_removed
from custom_components.hacs.repositories.removed import RemovedRepository
class HacsStatus: class HacsStatus:
@@ -40,7 +47,6 @@ class HacsCommon:
"""Common for HACS.""" """Common for HACS."""
categories = [] categories = []
blacklist = []
default = [] default = []
installed = [] installed = []
skip = [] skip = []
@@ -90,6 +96,7 @@ class Hacs:
github = None github = None
hass = None hass = None
version = None version = None
session = None
factory = HacsTaskFactory() factory = HacsTaskFactory()
system = System() system = System()
recuring_tasks = [] recuring_tasks = []
@@ -114,7 +121,7 @@ class Hacs:
"""Get repository by full_name.""" """Get repository by full_name."""
try: try:
for repository in self.repositories: for repository in self.repositories:
if repository.information.full_name == repository_full_name: if repository.data.full_name.lower() == repository_full_name.lower():
return repository return repository
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
pass pass
@@ -122,10 +129,9 @@ class Hacs:
def is_known(self, repository_full_name): def is_known(self, repository_full_name):
"""Return a bool if the repository is known.""" """Return a bool if the repository is known."""
for repository in self.repositories: return repository_full_name.lower() in [
if repository.information.full_name == repository_full_name: x.data.full_name.lower() for x in self.repositories
return True ]
return False
@property @property
def sorted_by_name(self): def sorted_by_name(self):
@@ -135,59 +141,16 @@ class Hacs:
@property @property
def sorted_by_repository_name(self): def sorted_by_repository_name(self):
"""Return a sorted(by repository_name) list of repository objects.""" """Return a sorted(by repository_name) list of repository objects."""
return sorted(self.repositories, key=lambda x: x.information.full_name) return sorted(self.repositories, key=lambda x: x.data.full_name)
async def register_repository(self, full_name, category, check=True): async def register_repository(self, full_name, category, check=True):
"""Register a repository.""" """Register a repository."""
from ..repositories.repository import RERPOSITORY_CLASSES await register_repository(full_name, category, check=True)
if full_name in self.common.skip:
if full_name != "hacs/integration":
self.logger.debug(f"Skipping {full_name}")
return
if category not in RERPOSITORY_CLASSES:
msg = f"{category} is not a valid repository category."
self.logger.error(msg)
raise HacsException(msg)
repository = RERPOSITORY_CLASSES[category](full_name)
if check:
try:
await repository.registration()
if self.system.status.new:
repository.status.new = False
if repository.validate.errors:
self.common.skip.append(repository.information.full_name)
if not self.system.status.startup:
self.logger.error(f"Validation for {full_name} failed.")
return repository.validate.errors
repository.logger.info("Registration complete")
except AIOGitHubException as exception:
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
self.common.skip.append(repository.information.full_name)
# if not self.system.status.startup:
if self.system.status.startup:
self.logger.error(
f"Validation for {full_name} failed with {exception}."
)
return exception
self.hass.bus.async_fire(
"hacs/repository",
{
"id": 1337,
"action": "registration",
"repository": repository.information.full_name,
"repository_id": repository.information.uid,
},
)
self.repositories.append(repository)
async def startup_tasks(self): async def startup_tasks(self):
"""Tasks tha are started after startup.""" """Tasks tha are started after startup."""
self.system.status.background_task = True self.system.status.background_task = True
await self.hass.async_add_executor_job(setup_extra_stores, self) await self.hass.async_add_executor_job(setup_extra_stores)
self.hass.bus.async_fire("hacs/status", {}) self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc) self.logger.debug(self.github.ratelimits.reset_utc)
@@ -195,7 +158,7 @@ class Hacs:
await self.handle_critical_repositories_startup() await self.handle_critical_repositories_startup()
await self.handle_critical_repositories() await self.handle_critical_repositories()
await self.load_known_repositories() await self.load_known_repositories()
await self.clear_out_blacklisted_repositories() await self.clear_out_removed_repositories()
self.recuring_tasks.append( self.recuring_tasks.append(
async_track_time_interval( async_track_time_interval(
@@ -257,7 +220,8 @@ class Hacs:
stored_critical = [] stored_critical = []
for repository in critical: for repository in critical:
self.common.blacklist.append(repository["repository"]) removed_repo = get_removed(repository["repository"])
removed_repo.removal_type = "critical"
repo = self.get_by_name(repository["repository"]) repo = self.get_by_name(repository["repository"])
stored = { stored = {
@@ -266,7 +230,6 @@ class Hacs:
"link": repository["link"], "link": repository["link"],
"acknowledged": True, "acknowledged": True,
} }
if repository["repository"] not in instored: if repository["repository"] not in instored:
if repo is not None and repo.installed: if repo is not None and repo.installed:
self.logger.critical( self.logger.critical(
@@ -278,6 +241,7 @@ class Hacs:
repo.remove() repo.remove()
await repo.uninstall() await repo.uninstall()
stored_critical.append(stored) stored_critical.append(stored)
removed_repo.update_data(stored)
# Save to FS # Save to FS
await async_save_to_store(self.hass, "critical", stored_critical) await async_save_to_store(self.hass, "critical", stored_critical)
@@ -299,7 +263,7 @@ class Hacs:
for repository in self.repositories: for repository in self.repositories:
if ( if (
repository.status.installed repository.status.installed
and repository.category in self.common.categories and repository.data.category in self.common.categories
): ):
self.factory.tasks.append(self.factory.safe_update(repository)) self.factory.tasks.append(self.factory.safe_update(repository))
@@ -313,33 +277,35 @@ class Hacs:
async def recuring_tasks_all(self, notarealarg=None): async def recuring_tasks_all(self, notarealarg=None):
"""Recuring tasks for all repositories.""" """Recuring tasks for all repositories."""
self.logger.debug("Starting recuring background task for all repositories") self.logger.debug("Starting recuring background task for all repositories")
await self.hass.async_add_executor_job(setup_extra_stores)
self.system.status.background_task = True self.system.status.background_task = True
self.hass.bus.async_fire("hacs/status", {}) self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc) self.logger.debug(self.github.ratelimits.reset_utc)
for repository in self.repositories: for repository in self.repositories:
if repository.category in self.common.categories: if repository.data.category in self.common.categories:
self.factory.tasks.append(self.factory.safe_common_update(repository)) self.factory.tasks.append(self.factory.safe_common_update(repository))
await self.factory.execute() await self.factory.execute()
await self.load_known_repositories() await self.load_known_repositories()
await self.clear_out_blacklisted_repositories() await self.clear_out_removed_repositories()
self.system.status.background_task = False self.system.status.background_task = False
await self.data.async_write() await self.data.async_write()
self.hass.bus.async_fire("hacs/status", {}) self.hass.bus.async_fire("hacs/status", {})
self.hass.bus.async_fire("hacs/repository", {"action": "reload"}) self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
self.logger.debug("Recuring background task for all repositories done") self.logger.debug("Recuring background task for all repositories done")
async def clear_out_blacklisted_repositories(self): async def clear_out_removed_repositories(self):
"""Clear out blaclisted repositories.""" """Clear out blaclisted repositories."""
need_to_save = False need_to_save = False
for repository in self.common.blacklist: for removed in removed_repositories:
if self.is_known(repository): if self.is_known(removed.repository):
repository = self.get_by_name(repository) repository = self.get_by_name(removed.repository)
if repository.status.installed: if repository.status.installed and removed.removal_type != "critical":
self.logger.warning( self.logger.warning(
f"You have {repository.information.full_name} installed with HACS " f"You have {repository.data.full_name} installed with HACS "
+ "this repository has been blacklisted, please consider removing it." + f"this repository has been removed, please consider removing it. "
+ f"Removal reason ({removed.removal_type})"
) )
else: else:
need_to_save = True need_to_save = True
@@ -353,7 +319,7 @@ class Hacs:
repositories = {} repositories = {}
for category in self.common.categories: for category in self.common.categories:
repositories[category] = await get_default_repos_lists( repositories[category] = await get_default_repos_lists(
self.github, category self.session, self.configuration.token, category
) )
org = await get_default_repos_orgs(self.github, category) org = await get_default_repos_orgs(self.github, category)
for repo in org: for repo in org:
@@ -370,17 +336,20 @@ class Hacs:
self.logger.info("Loading known repositories") self.logger.info("Loading known repositories")
repositories = await self.get_repositories() repositories = await self.get_repositories()
for item in await get_default_repos_lists(self.github, "blacklist"): for item in await get_default_repos_lists(
if item not in self.common.blacklist: self.session, self.configuration.token, "removed"
self.common.blacklist.append(item) ):
removed = get_removed(item["repository"])
removed.reason = item.get("reason")
removed.link = item.get("link")
removed.removal_type = item.get("removal_type")
for category in repositories: for category in repositories:
for repo in repositories[category]: for repo in repositories[category]:
if repo in self.common.blacklist: if is_removed(repo):
continue continue
if self.is_known(repo): if self.is_known(repo):
continue continue
self.factory.tasks.append( self.factory.tasks.append(self.factory.safe_register(repo, category))
self.factory.safe_register(self, repo, category)
)
await self.factory.execute() await self.factory.execute()
self.logger.info("Loading known repositories finished")

45
config/custom_components/hacs/hacsbase/backup.py Executable file → Normal file
View File

@@ -70,3 +70,48 @@ class Backup:
while os.path.exists(self.backup_path): while os.path.exists(self.backup_path):
sleep(0.1) sleep(0.1)
self.logger.debug(f"Backup dir {self.backup_path} cleared") self.logger.debug(f"Backup dir {self.backup_path} cleared")
class BackupNetDaemon:
"""BackupNetDaemon."""
def __init__(self, repository):
"""Initialize."""
self.repository = repository
self.logger = Logger("hacs.backup")
self.backup_path = (
tempfile.gettempdir() + "/hacs_persistent_netdaemon/" + repository.data.name
)
def create(self):
"""Create a backup in /tmp"""
if os.path.exists(self.backup_path):
shutil.rmtree(self.backup_path)
while os.path.exists(self.backup_path):
sleep(0.1)
os.makedirs(self.backup_path, exist_ok=True)
for filename in os.listdir(self.repository.content.path.local):
if filename.endswith(".yaml"):
source_file_name = f"{self.repository.content.path.local}/{filename}"
target_file_name = f"{self.backup_path}/{filename}"
shutil.copyfile(source_file_name, target_file_name)
def restore(self):
"""Create a backup in /tmp"""
if os.path.exists(self.backup_path):
for filename in os.listdir(self.backup_path):
if filename.endswith(".yaml"):
source_file_name = f"{self.backup_path}/{filename}"
target_file_name = (
f"{self.repository.content.path.local}/{filename}"
)
shutil.copyfile(source_file_name, target_file_name)
def cleanup(self):
"""Create a backup in /tmp"""
if os.path.exists(self.backup_path):
shutil.rmtree(self.backup_path)
while os.path.exists(self.backup_path):
sleep(0.1)
self.logger.debug(f"Backup dir {self.backup_path} cleared")

View File

@@ -11,6 +11,8 @@ class Configuration:
# Main configuration: # Main configuration:
appdaemon_path: str = "appdaemon/apps/" appdaemon_path: str = "appdaemon/apps/"
appdaemon: bool = False appdaemon: bool = False
netdaemon_path: str = "netdaemon/apps/"
netdaemon: bool = False
config: dict = {} config: dict = {}
config_entry: dict = {} config_entry: dict = {}
config_type: str = None config_type: str = None

0
config/custom_components/hacs/hacsbase/const.py Executable file → Normal file
View File

82
config/custom_components/hacs/hacsbase/data.py Executable file → Normal file
View File

@@ -1,50 +1,57 @@
"""Data handler for HACS.""" """Data handler for HACS."""
from integrationhelper import Logger from integrationhelper import Logger
from . import Hacs
from ..const import VERSION from ..const import VERSION
from ..repositories.repository import HacsRepository from ..repositories.repository import HacsRepository
from ..repositories.manifest import HacsManifest from ..repositories.manifest import HacsManifest
from ..store import async_save_to_store, async_load_from_store from ..store import async_save_to_store, async_load_from_store
from custom_components.hacs.globals import get_hacs, removed_repositories, get_removed
from custom_components.hacs.helpers.register_repository import register_repository
class HacsData(Hacs):
class HacsData:
"""HacsData class.""" """HacsData class."""
def __init__(self): def __init__(self):
"""Initialize.""" """Initialize."""
self.logger = Logger("hacs.data") self.logger = Logger("hacs.data")
self.hacs = get_hacs()
async def async_write(self): async def async_write(self):
"""Write content to the store files.""" """Write content to the store files."""
if self.system.status.background_task or self.system.disabled: if self.hacs.system.status.background_task or self.hacs.system.disabled:
return return
self.logger.debug("Saving data") self.logger.debug("Saving data")
# Hacs # Hacs
await async_save_to_store( await async_save_to_store(
self.hass, self.hacs.hass,
"hacs", "hacs",
{ {
"view": self.configuration.frontend_mode, "view": self.hacs.configuration.frontend_mode,
"compact": self.configuration.frontend_compact, "compact": self.hacs.configuration.frontend_compact,
"onboarding_done": self.configuration.onboarding_done, "onboarding_done": self.hacs.configuration.onboarding_done,
}, },
) )
await async_save_to_store(
self.hacs.hass, "removed", [x.__dict__ for x in removed_repositories]
)
# Repositories # Repositories
content = {} content = {}
for repository in self.repositories: for repository in self.hacs.repositories:
if repository.repository_manifest is not None: if repository.repository_manifest is not None:
repository_manifest = repository.repository_manifest.manifest repository_manifest = repository.repository_manifest.manifest
else: else:
repository_manifest = None repository_manifest = None
content[repository.information.uid] = { content[repository.information.uid] = {
"authors": repository.information.authors, "authors": repository.data.authors,
"category": repository.information.category, "category": repository.data.category,
"description": repository.information.description, "description": repository.data.description,
"downloads": repository.releases.last_release_object_downloads, "downloads": repository.releases.downloads,
"full_name": repository.information.full_name, "full_name": repository.data.full_name,
"first_install": repository.status.first_install, "first_install": repository.status.first_install,
"hide": repository.status.hide, "hide": repository.status.hide,
"installed_commit": repository.versions.installed_commit, "installed_commit": repository.versions.installed_commit,
@@ -52,54 +59,56 @@ class HacsData(Hacs):
"last_commit": repository.versions.available_commit, "last_commit": repository.versions.available_commit,
"last_release_tag": repository.versions.available, "last_release_tag": repository.versions.available,
"last_updated": repository.information.last_updated, "last_updated": repository.information.last_updated,
"name": repository.information.name, "name": repository.data.name,
"new": repository.status.new, "new": repository.status.new,
"repository_manifest": repository_manifest, "repository_manifest": repository_manifest,
"selected_tag": repository.status.selected_tag, "selected_tag": repository.status.selected_tag,
"show_beta": repository.status.show_beta, "show_beta": repository.status.show_beta,
"stars": repository.information.stars, "stars": repository.data.stargazers_count,
"topics": repository.information.topics, "topics": repository.data.topics,
"version_installed": repository.versions.installed, "version_installed": repository.versions.installed,
} }
await async_save_to_store(self.hass, "repositories", content) await async_save_to_store(self.hacs.hass, "repositories", content)
self.hass.bus.async_fire("hacs/repository", {}) self.hacs.hass.bus.async_fire("hacs/repository", {})
self.hass.bus.fire("hacs/config", {}) self.hacs.hass.bus.fire("hacs/config", {})
async def restore(self): async def restore(self):
"""Restore saved data.""" """Restore saved data."""
hacs = await async_load_from_store(self.hass, "hacs") hacs = await async_load_from_store(self.hacs.hass, "hacs")
repositories = await async_load_from_store(self.hass, "repositories") repositories = await async_load_from_store(self.hacs.hass, "repositories")
removed = await async_load_from_store(self.hacs.hass, "removed")
try: try:
if not hacs and not repositories: if not hacs and not repositories:
# Assume new install # Assume new install
self.system.status.new = True self.hacs.system.status.new = True
return True return True
self.logger.info("Restore started") self.logger.info("Restore started")
# Hacs # Hacs
self.configuration.frontend_mode = hacs.get("view", "Grid") self.hacs.configuration.frontend_mode = hacs.get("view", "Grid")
self.configuration.frontend_compact = hacs.get("compact", False) self.hacs.configuration.frontend_compact = hacs.get("compact", False)
self.configuration.onboarding_done = hacs.get("onboarding_done", False) self.hacs.configuration.onboarding_done = hacs.get("onboarding_done", False)
for entry in removed:
removed_repo = get_removed(entry["repository"])
removed_repo.update_data(entry)
# Repositories # Repositories
for entry in repositories: for entry in repositories:
repo = repositories[entry] repo = repositories[entry]
if repo["full_name"] == "hacs/integration": if not self.hacs.is_known(repo["full_name"]):
# Skip the old repo location await register_repository(
continue
if not self.is_known(repo["full_name"]):
await self.register_repository(
repo["full_name"], repo["category"], False repo["full_name"], repo["category"], False
) )
repository = self.get_by_name(repo["full_name"]) repository = self.hacs.get_by_name(repo["full_name"])
if repository is None: if repository is None:
self.logger.error(f"Did not find {repo['full_name']}") self.logger.error(f"Did not find {repo['full_name']}")
continue continue
# Restore repository attributes # Restore repository attributes
repository.information.uid = entry repository.information.uid = entry
await self.hass.async_add_executor_job( await self.hacs.hass.async_add_executor_job(
restore_repository_data, repository, repo restore_repository_data, repository, repo
) )
@@ -114,13 +123,12 @@ def restore_repository_data(
repository: type(HacsRepository), repository_data: dict repository: type(HacsRepository), repository_data: dict
) -> None: ) -> None:
"""Restore Repository Data""" """Restore Repository Data"""
repository.information.authors = repository_data.get("authors", []) repository.data.authors = repository_data.get("authors", [])
repository.information.description = repository_data.get("description") repository.data.description = repository_data.get("description")
repository.information.name = repository_data.get("name")
repository.releases.last_release_object_downloads = repository_data.get("downloads") repository.releases.last_release_object_downloads = repository_data.get("downloads")
repository.information.last_updated = repository_data.get("last_updated") repository.information.last_updated = repository_data.get("last_updated")
repository.information.topics = repository_data.get("topics", []) repository.data.topics = repository_data.get("topics", [])
repository.information.stars = repository_data.get("stars", 0) repository.data.stargazers_count = repository_data.get("stars", 0)
repository.releases.last_release = repository_data.get("last_release_tag") repository.releases.last_release = repository_data.get("last_release_tag")
repository.status.hide = repository_data.get("hide", False) repository.status.hide = repository_data.get("hide", False)
repository.status.installed = repository_data.get("installed", False) repository.status.installed = repository_data.get("installed", False)

4
config/custom_components/hacs/hacsbase/exceptions.py Executable file → Normal file
View File

@@ -3,3 +3,7 @@
class HacsException(Exception): class HacsException(Exception):
"""Super basic.""" """Super basic."""
class HacsExpectedException(HacsException):
"""For stuff that are expected."""

20
config/custom_components/hacs/hacsbase/task_factory.py Executable file → Normal file
View File

@@ -5,6 +5,10 @@ from datetime import timedelta
import asyncio import asyncio
from aiogithubapi import AIOGitHubException from aiogithubapi import AIOGitHubException
from custom_components.hacs.hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.register_repository import register_repository
max_concurrent_tasks = asyncio.Semaphore(15) max_concurrent_tasks = asyncio.Semaphore(15)
sleeper = 5 sleeper = 5
@@ -44,8 +48,8 @@ class HacsTaskFactory:
async with max_concurrent_tasks: async with max_concurrent_tasks:
try: try:
await repository.common_update() await repository.common_update()
except AIOGitHubException as exception: except (AIOGitHubException, HacsException) as exception:
logger.error(exception) logger.error("%s - %s", repository.data.full_name, exception)
# Due to GitHub ratelimits we need to sleep a bit # Due to GitHub ratelimits we need to sleep a bit
await asyncio.sleep(sleeper) await asyncio.sleep(sleeper)
@@ -54,18 +58,18 @@ class HacsTaskFactory:
async with max_concurrent_tasks: async with max_concurrent_tasks:
try: try:
await repository.update_repository() await repository.update_repository()
except AIOGitHubException as exception: except (AIOGitHubException, HacsException) as exception:
logger.error(exception) logger.error("%s - %s", repository.data.full_name, exception)
# Due to GitHub ratelimits we need to sleep a bit # Due to GitHub ratelimits we need to sleep a bit
await asyncio.sleep(sleeper) await asyncio.sleep(sleeper)
async def safe_register(self, hacs, repo, category): async def safe_register(self, repo, category):
async with max_concurrent_tasks: async with max_concurrent_tasks:
try: try:
await hacs.register_repository(repo, category) await register_repository(repo, category)
except AIOGitHubException as exception: except (AIOGitHubException, HacsException) as exception:
logger.error(exception) logger.error("%s - %s", repo, exception)
# Due to GitHub ratelimits we need to sleep a bit # Due to GitHub ratelimits we need to sleep a bit
await asyncio.sleep(sleeper) await asyncio.sleep(sleeper)

0
config/custom_components/hacs/handler/__init__.py Executable file → Normal file
View File

10
config/custom_components/hacs/handler/download.py Executable file → Normal file
View File

@@ -7,15 +7,17 @@ import aiofiles
import async_timeout import async_timeout
from integrationhelper import Logger from integrationhelper import Logger
import backoff import backoff
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from ..hacsbase.exceptions import HacsException from ..hacsbase.exceptions import HacsException
from custom_components.hacs.globals import get_hacs
@backoff.on_exception(backoff.expo, Exception, max_tries=5) @backoff.on_exception(backoff.expo, Exception, max_tries=5)
async def async_download_file(hass, url): async def async_download_file(url):
""" """
Download files, and return the content. Download files, and return the content.
""" """
hacs = get_hacs()
logger = Logger("hacs.download.downloader") logger = Logger("hacs.download.downloader")
if url is None: if url is None:
return return
@@ -28,8 +30,8 @@ async def async_download_file(hass, url):
result = None result = None
with async_timeout.timeout(60, loop=hass.loop): with async_timeout.timeout(60, loop=hacs.hass.loop):
request = await async_get_clientsession(hass).get(url) request = await hacs.session.get(url)
# Make sure that we got a valid result # Make sure that we got a valid result
if request.status == 200: if request.status == 200:

0
config/custom_components/hacs/handler/template.py Executable file → Normal file
View File

124
config/custom_components/hacs/helpers/download.py Executable file → Normal file
View File

@@ -16,9 +16,13 @@ class FileInformation:
def should_try_releases(repository): def should_try_releases(repository):
"""Return a boolean indicating whether to download releases or not.""" """Return a boolean indicating whether to download releases or not."""
if repository.ref == repository.information.default_branch: if repository.data.zip_release:
if repository.data.filename.endswith(".zip"):
if repository.ref != repository.data.default_branch:
return True
if repository.ref == repository.data.default_branch:
return False return False
if repository.information.category not in ["plugin", "theme"]: if repository.data.category not in ["plugin", "theme"]:
return False return False
if not repository.releases.releases: if not repository.releases.releases:
return False return False
@@ -31,7 +35,7 @@ def gather_files_to_download(repository):
tree = repository.tree tree = repository.tree
ref = f"{repository.ref}".replace("tags/", "") ref = f"{repository.ref}".replace("tags/", "")
releaseobjects = repository.releases.objects releaseobjects = repository.releases.objects
category = repository.information.category category = repository.data.category
remotelocation = repository.content.path.remote remotelocation = repository.content.path.remote
if should_try_releases(repository): if should_try_releases(repository):
@@ -44,7 +48,7 @@ def gather_files_to_download(repository):
if repository.content.single: if repository.content.single:
for treefile in tree: for treefile in tree:
if treefile.filename == repository.information.file_name: if treefile.filename == repository.data.file_name:
files.append( files.append(
FileInformation( FileInformation(
treefile.download_url, treefile.full_path, treefile.filename treefile.download_url, treefile.full_path, treefile.filename
@@ -55,28 +59,29 @@ def gather_files_to_download(repository):
if category == "plugin": if category == "plugin":
for treefile in tree: for treefile in tree:
if treefile.path in ["", "dist"]: if treefile.path in ["", "dist"]:
if not remotelocation:
if treefile.filename != repository.information.file_name:
continue
if remotelocation == "dist" and not treefile.filename.startswith( if remotelocation == "dist" and not treefile.filename.startswith(
"dist" "dist"
): ):
continue continue
if treefile.is_directory: if not remotelocation:
continue if not treefile.filename.endswith(".js"):
files.append( continue
FileInformation( if treefile.path != "":
treefile.download_url, treefile.full_path, treefile.filename continue
if not treefile.is_directory:
files.append(
FileInformation(
treefile.download_url, treefile.full_path, treefile.filename
)
) )
)
if files: if files:
return files return files
if repository.repository_manifest.content_in_root: if repository.data.content_in_root:
if repository.repository_manifest.filename is None: if not repository.data.filename:
if category == "theme": if category == "theme":
tree = filter_content_return_one_of_type( tree = filter_content_return_one_of_type(
repository.tree, "themes", "yaml", "full_path" repository.tree, "", "yaml", "full_path"
) )
for path in tree: for path in tree:
@@ -104,21 +109,17 @@ async def download_zip(repository, validate):
return validate return validate
for content in contents: for content in contents:
filecontent = await async_download_file( filecontent = await async_download_file(content.download_url)
repository.hass, content.download_url
)
if filecontent is None: if filecontent is None:
validate.errors.append(f"[{content.name}] was not downloaded.") validate.errors.append(f"[{content.name}] was not downloaded.")
continue continue
result = await async_save_file( result = await async_save_file(
f"{tempfile.gettempdir()}/{repository.repository_manifest.filename}", f"{tempfile.gettempdir()}/{repository.data.filename}", filecontent
filecontent,
) )
with zipfile.ZipFile( with zipfile.ZipFile(
f"{tempfile.gettempdir()}/{repository.repository_manifest.filename}", f"{tempfile.gettempdir()}/{repository.data.filename}", "r"
"r",
) as zip_file: ) as zip_file:
zip_file.extractall(repository.content.path.local) zip_file.extractall(repository.content.path.local)
@@ -132,54 +133,49 @@ async def download_zip(repository, validate):
return validate return validate
async def download_content(repository, validate, local_directory): async def download_content(repository):
"""Download the content of a directory.""" """Download the content of a directory."""
contents = gather_files_to_download(repository) contents = gather_files_to_download(repository)
try: repository.logger.debug(repository.data.filename)
if not contents: if not contents:
raise HacsException("No content to download") raise HacsException("No content to download")
for content in contents: for content in contents:
if repository.repository_manifest.content_in_root: if repository.data.content_in_root and repository.data.filename:
if repository.repository_manifest.filename is not None: if content.name != repository.data.filename:
if content.name != repository.repository_manifest.filename:
continue
repository.logger.debug(f"Downloading {content.name}")
filecontent = await async_download_file(
repository.hass, content.download_url
)
if filecontent is None:
validate.errors.append(f"[{content.name}] was not downloaded.")
continue continue
repository.logger.debug(f"Downloading {content.name}")
# Save the content of the file. filecontent = await async_download_file(content.download_url)
if repository.content.single or content.path is None:
local_directory = repository.content.path.local
else: if filecontent is None:
_content_path = content.path repository.validate.errors.append(f"[{content.name}] was not downloaded.")
if not repository.repository_manifest.content_in_root: continue
_content_path = _content_path.replace(
f"{repository.content.path.remote}", ""
)
local_directory = f"{repository.content.path.local}/{_content_path}" # Save the content of the file.
local_directory = local_directory.split("/") if repository.content.single or content.path is None:
del local_directory[-1] local_directory = repository.content.path.local
local_directory = "/".join(local_directory)
# Check local directory else:
pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) _content_path = content.path
if not repository.data.content_in_root:
_content_path = _content_path.replace(
f"{repository.content.path.remote}", ""
)
local_file_path = f"{local_directory}/{content.name}" local_directory = f"{repository.content.path.local}/{_content_path}"
result = await async_save_file(local_file_path, filecontent) local_directory = local_directory.split("/")
if result: del local_directory[-1]
repository.logger.info(f"download of {content.name} complete") local_directory = "/".join(local_directory)
continue
validate.errors.append(f"[{content.name}] was not downloaded.") # Check local directory
pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True)
local_file_path = (f"{local_directory}/{content.name}").replace("//", "/")
result = await async_save_file(local_file_path, filecontent)
if result:
repository.logger.info(f"download of {content.name} complete")
continue
repository.validate.errors.append(f"[{content.name}] was not downloaded.")
except Exception as exception: # pylint: disable=broad-except
validate.errors.append(f"Download was not complete [{exception}]")
return validate

11
config/custom_components/hacs/helpers/filters.py Executable file → Normal file
View File

@@ -42,3 +42,14 @@ def find_first_of_filetype(content, filterfiltype, attr="name"):
filename = getattr(_filename, attr) filename = getattr(_filename, attr)
break break
return filename return filename
def get_first_directory_in_directory(content, dirname):
"""Return the first directory in dirname or None."""
directory = None
for path in content:
if path.full_path.startswith(dirname) and path.full_path != dirname:
if path.is_directory:
directory = path.filename
break
return directory

5
config/custom_components/hacs/helpers/get_defaults.py Executable file → Normal file
View File

@@ -2,6 +2,7 @@
import json import json
from aiogithubapi import AIOGitHub, AIOGitHubException from aiogithubapi import AIOGitHub, AIOGitHubException
from integrationhelper import Logger from integrationhelper import Logger
from custom_components.hacs.helpers.information import get_repository
async def get_default_repos_orgs(github: type(AIOGitHub), category: str) -> dict: async def get_default_repos_orgs(github: type(AIOGitHub), category: str) -> dict:
@@ -27,13 +28,13 @@ async def get_default_repos_orgs(github: type(AIOGitHub), category: str) -> dict
return repositories return repositories
async def get_default_repos_lists(github: type(AIOGitHub), default: str) -> dict: async def get_default_repos_lists(session, token, default: str) -> dict:
"""Gets repositories from default list.""" """Gets repositories from default list."""
repositories = [] repositories = []
logger = Logger("hacs") logger = Logger("hacs")
try: try:
repo = await github.get_repo("hacs/default") repo = await get_repository(session, token, "hacs/default")
content = await repo.get_contents(default) content = await repo.get_contents(default)
repositories = json.loads(content.content) repositories = json.loads(content.content)

View File

@@ -0,0 +1,184 @@
"""Return repository information if any."""
import json
from aiogithubapi import AIOGitHubException, AIOGitHub
from custom_components.hacs.handler.template import render_template
from custom_components.hacs.hacsbase.exceptions import HacsException
def info_file(repository):
"""get info filename."""
if repository.data.render_readme:
for filename in ["readme", "readme.md", "README", "README.md", "README.MD"]:
if filename in repository.treefiles:
return filename
return ""
for filename in ["info", "info.md", "INFO", "INFO.md", "INFO.MD"]:
if filename in repository.treefiles:
return filename
return ""
async def get_info_md_content(repository):
"""Get the content of info.md"""
filename = info_file(repository)
if not filename:
return ""
try:
info = await repository.repository_object.get_contents(filename, repository.ref)
if info is None:
return ""
info = info.content.replace("<svg", "<disabled").replace("</svg", "</disabled")
return render_template(info, repository)
except (AIOGitHubException, Exception): # pylint: disable=broad-except
return ""
async def get_repository(session, token, repository_full_name):
"""Return a repository object or None."""
try:
github = AIOGitHub(token, session)
repository = await github.get_repo(repository_full_name)
return repository
except AIOGitHubException as exception:
raise HacsException(exception)
async def get_tree(repository, ref):
"""Return the repository tree."""
try:
tree = await repository.get_tree(ref)
return tree
except AIOGitHubException as exception:
raise HacsException(exception)
async def get_releases(repository, prerelease=False, returnlimit=5):
"""Return the repository releases."""
try:
releases = await repository.get_releases(prerelease, returnlimit)
return releases
except AIOGitHubException as exception:
raise HacsException(exception)
async def get_integration_manifest(repository):
"""Return the integration manifest."""
if repository.data.content_in_root:
manifest_path = "manifest.json"
else:
manifest_path = f"{repository.content.path.remote}/manifest.json"
if not manifest_path in [x.full_path for x in repository.tree]:
raise HacsException(f"No file found '{manifest_path}'")
try:
manifest = await repository.repository_object.get_contents(
manifest_path, repository.ref
)
manifest = json.loads(manifest.content)
except Exception as exception: # pylint: disable=broad-except
raise HacsException(f"Could not read manifest.json [{exception}]")
try:
repository.integration_manifest = manifest
repository.data.authors = manifest["codeowners"]
repository.data.domain = manifest["domain"]
repository.data.manifest_name = manifest["name"]
repository.data.homeassistant = manifest.get("homeassistant")
# Set local path
repository.content.path.local = repository.localpath
except KeyError as exception:
raise HacsException(f"Missing expected key {exception} in 'manifest.json'")
def find_file_name(repository):
"""Get the filename to target."""
if repository.data.category == "plugin":
get_file_name_plugin(repository)
elif repository.data.category == "integration":
get_file_name_integration(repository)
elif repository.data.category == "theme":
get_file_name_theme(repository)
elif repository.data.category == "appdaemon":
get_file_name_appdaemon(repository)
elif repository.data.category == "python_script":
get_file_name_python_script(repository)
def get_file_name_plugin(repository):
"""Get the filename to target."""
tree = repository.tree
releases = repository.releases.objects
if repository.data.content_in_root:
possible_locations = [""]
else:
possible_locations = ["release", "dist", ""]
# Handler for plug requirement 3
if repository.data.filename:
valid_filenames = [repository.data.filename]
else:
valid_filenames = [
f"{repository.data.name.replace('lovelace-', '')}.js",
f"{repository.data.name}.js",
f"{repository.data.name}.umd.js",
f"{repository.data.name}-bundle.js",
]
for location in possible_locations:
if location == "release":
if not releases:
continue
release = releases[0]
if not release.assets:
continue
asset = release.assets[0]
for filename in valid_filenames:
if filename == asset.name:
repository.data.file_name = filename
repository.content.path.remote = "release"
break
else:
for filename in valid_filenames:
if f"{location+'/' if location else ''}{filename}" in [
x.full_path for x in tree
]:
repository.data.file_name = filename.split("/")[-1]
repository.content.path.remote = location
break
def get_file_name_integration(repository):
"""Get the filename to target."""
tree = repository.tree
releases = repository.releases.objects
def get_file_name_theme(repository):
"""Get the filename to target."""
tree = repository.tree
for treefile in tree:
if treefile.full_path.startswith(
repository.content.path.remote
) and treefile.full_path.endswith(".yaml"):
repository.data.file_name = treefile.filename
def get_file_name_appdaemon(repository):
"""Get the filename to target."""
tree = repository.tree
releases = repository.releases.objects
def get_file_name_python_script(repository):
"""Get the filename to target."""
tree = repository.tree
for treefile in tree:
if treefile.full_path.startswith(
repository.content.path.remote
) and treefile.full_path.endswith(".py"):
repository.data.file_name = treefile.filename

92
config/custom_components/hacs/helpers/install.py Executable file → Normal file
View File

@@ -1,14 +1,17 @@
"""Install helper for repositories.""" """Install helper for repositories."""
import os import os
import tempfile import tempfile
from custom_components.hacs.globals import get_hacs
from custom_components.hacs.hacsbase.exceptions import HacsException from custom_components.hacs.hacsbase.exceptions import HacsException
from custom_components.hacs.hacsbase.backup import Backup from custom_components.hacs.hacsbase.backup import Backup, BackupNetDaemon
from custom_components.hacs.helpers.download import download_content
async def install_repository(repository): async def install_repository(repository):
"""Common installation steps of the repository.""" """Common installation steps of the repository."""
persistent_directory = None persistent_directory = None
await repository.update_repository() await repository.update_repository()
repository.validate.errors = []
if not repository.can_install: if not repository.can_install:
raise HacsException( raise HacsException(
@@ -16,41 +19,36 @@ async def install_repository(repository):
) )
version = version_to_install(repository) version = version_to_install(repository)
if version == repository.information.default_branch: if version == repository.data.default_branch:
repository.ref = version repository.ref = version
else: else:
repository.ref = f"tags/{version}" repository.ref = f"tags/{version}"
if repository.repository_manifest: if repository.status.installed and repository.data.category == "netdaemon":
if repository.repository_manifest.persistent_directory: persistent_directory = BackupNetDaemon(repository)
if os.path.exists( persistent_directory.create()
f"{repository.content.path.local}/{repository.repository_manifest.persistent_directory}"
): elif repository.data.persistent_directory:
persistent_directory = Backup( if os.path.exists(
f"{repository.content.path.local}/{repository.repository_manifest.persistent_directory}", f"{repository.content.path.local}/{repository.data.persistent_directory}"
tempfile.gettempdir() + "/hacs_persistent_directory/", ):
) persistent_directory = Backup(
persistent_directory.create() f"{repository.content.path.local}/{repository.data.persistent_directory}",
tempfile.gettempdir() + "/hacs_persistent_directory/",
)
persistent_directory.create()
if repository.status.installed and not repository.content.single: if repository.status.installed and not repository.content.single:
backup = Backup(repository.content.path.local) backup = Backup(repository.content.path.local)
backup.create() backup.create()
if ( if repository.data.zip_release and version != repository.data.default_branch:
repository.repository_manifest.zip_release await repository.download_zip(repository)
and version != repository.information.default_branch
):
validate = await repository.download_zip(repository.validate)
else: else:
validate = await repository.download_content( await download_content(repository)
repository.validate,
repository.content.path.remote,
repository.content.path.local,
repository.ref,
)
if validate.errors: if repository.validate.errors:
for error in validate.errors: for error in repository.validate.errors:
repository.logger.error(error) repository.logger.error(error)
if repository.status.installed and not repository.content.single: if repository.status.installed and not repository.content.single:
backup.restore() backup.restore()
@@ -62,14 +60,14 @@ async def install_repository(repository):
persistent_directory.restore() persistent_directory.restore()
persistent_directory.cleanup() persistent_directory.cleanup()
if validate.success: if repository.validate.success:
if repository.information.full_name not in repository.common.installed: if repository.data.full_name not in repository.hacs.common.installed:
if repository.information.full_name == "hacs/integration": if repository.data.full_name == "hacs/integration":
repository.common.installed.append(repository.information.full_name) repository.hacs.common.installed.append(repository.data.full_name)
repository.status.installed = True repository.status.installed = True
repository.versions.installed_commit = repository.versions.available_commit repository.versions.installed_commit = repository.versions.available_commit
if version == repository.information.default_branch: if version == repository.data.default_branch:
repository.versions.installed = None repository.versions.installed = None
else: else:
repository.versions.installed = version repository.versions.installed = version
@@ -80,28 +78,34 @@ async def install_repository(repository):
async def reload_after_install(repository): async def reload_after_install(repository):
"""Reload action after installation success.""" """Reload action after installation success."""
if repository.information.category == "integration": if repository.data.category == "integration":
if repository.config_flow: if repository.config_flow:
if repository.information.full_name != "hacs/integration": if repository.data.full_name != "hacs/integration":
await repository.reload_custom_components() await repository.reload_custom_components()
repository.pending_restart = True repository.pending_restart = True
elif repository.information.category == "theme": elif repository.data.category == "theme":
try: try:
await repository.hass.services.async_call("frontend", "reload_themes", {}) await repository.hacs.hass.services.async_call(
"frontend", "reload_themes", {}
)
except Exception: # pylint: disable=broad-except
pass
elif repository.data.category == "netdaemon":
try:
await repository.hacs.hass.services.async_call(
"hassio", "addon_restart", {"addon": "e466aeb3_netdaemon"}
)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
pass pass
def installation_complete(repository): def installation_complete(repository):
"""Action to run when the installation is complete.""" """Action to run when the installation is complete."""
repository.hass.bus.async_fire( hacs = get_hacs()
hacs.hass.bus.async_fire(
"hacs/repository", "hacs/repository",
{ {"id": 1337, "action": "install", "repository": repository.data.full_name},
"id": 1337,
"action": "install",
"repository": repository.information.full_name,
},
) )
@@ -115,10 +119,10 @@ def version_to_install(repository):
return repository.status.selected_tag return repository.status.selected_tag
return repository.versions.available return repository.versions.available
if repository.status.selected_tag is not None: if repository.status.selected_tag is not None:
if repository.status.selected_tag == repository.information.default_branch: if repository.status.selected_tag == repository.data.default_branch:
return repository.information.default_branch return repository.data.default_branch
if repository.status.selected_tag in repository.releases.published_tags: if repository.status.selected_tag in repository.releases.published_tags:
return repository.status.selected_tag return repository.status.selected_tag
if repository.information.default_branch is None: if repository.data.default_branch is None:
return "master" return "master"
return repository.information.default_branch return repository.data.default_branch

23
config/custom_components/hacs/helpers/misc.py Executable file → Normal file
View File

@@ -2,20 +2,23 @@
import semantic_version import semantic_version
def get_repository_name( def get_repository_name(repository) -> str:
hacs_manifest, repository_name: str, category: str = None, manifest: dict = None
) -> str:
"""Return the name of the repository for use in the frontend.""" """Return the name of the repository for use in the frontend."""
if hacs_manifest.name is not None: if repository.repository_manifest.name is not None:
return hacs_manifest.name return repository.repository_manifest.name
if category == "integration": if repository.data.category == "integration":
if manifest: if repository.integration_manifest:
if "name" in manifest: if "name" in repository.integration_manifest:
return manifest["name"] return repository.integration_manifest["name"]
return repository_name.replace("-", " ").replace("_", " ").title() return (
repository.data.full_name.split("/")[-1]
.replace("-", " ")
.replace("_", " ")
.title()
)
def version_left_higher_then_right(new: str, old: str) -> bool: def version_left_higher_then_right(new: str, old: str) -> bool:

View File

@@ -0,0 +1,8 @@
"""Verify network."""
from socket import gaierror
from integrationhelper import Logger
def internet_connectivity_check(host="api.github.com"):
"""Verify network connectivity."""
return True

View File

@@ -0,0 +1,49 @@
"""Register a repository."""
from aiogithubapi import AIOGitHubException
from custom_components.hacs.globals import get_hacs
from custom_components.hacs.hacsbase.exceptions import (
HacsException,
HacsExpectedException,
)
async def register_repository(full_name, category, check=True):
"""Register a repository."""
hacs = get_hacs()
from custom_components.hacs.repositories import (
RERPOSITORY_CLASSES,
) # To hanle import error
if full_name in hacs.common.skip:
if full_name != "hacs/integration":
raise HacsExpectedException(f"Skipping {full_name}")
if category not in RERPOSITORY_CLASSES:
raise HacsException(f"{category} is not a valid repository category.")
repository = RERPOSITORY_CLASSES[category](full_name)
if check:
try:
await repository.registration()
if hacs.system.status.new:
repository.status.new = False
if repository.validate.errors:
hacs.common.skip.append(repository.data.full_name)
if not hacs.system.status.startup:
hacs.logger.error(f"Validation for {full_name} failed.")
return repository.validate.errors
repository.logger.info("Registration complete")
except AIOGitHubException as exception:
hacs.common.skip.append(repository.data.full_name)
raise HacsException(f"Validation for {full_name} failed with {exception}.")
hacs.hass.bus.async_fire(
"hacs/repository",
{
"id": 1337,
"action": "registration",
"repository": repository.data.full_name,
"repository_id": repository.information.uid,
},
)
hacs.repositories.append(repository)

View File

@@ -0,0 +1,90 @@
"""Helper to do common validation for repositories."""
from aiogithubapi import AIOGitHubException
from custom_components.hacs.globals import get_hacs, is_removed
from custom_components.hacs.hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.install import version_to_install
from custom_components.hacs.helpers.information import (
get_repository,
get_tree,
get_releases,
)
async def common_validate(repository):
"""Common validation steps of the repository."""
repository.validate.errors = []
# Make sure the repository exist.
repository.logger.debug("Checking repository.")
await common_update_data(repository)
# Step 6: Get the content of hacs.json
await repository.get_repository_manifest_content()
async def common_update_data(repository):
"""Common update data."""
hacs = get_hacs()
try:
repository_object = await get_repository(
hacs.session, hacs.configuration.token, repository.data.full_name
)
repository.repository_object = repository_object
repository.data.update_data(repository_object.attributes)
except (AIOGitHubException, HacsException) as exception:
if not hacs.system.status.startup:
repository.logger.error(exception)
repository.validate.errors.append("Repository does not exist.")
raise HacsException(exception)
# Make sure the repository is not archived.
if repository.data.archived:
repository.validate.errors.append("Repository is archived.")
raise HacsException("Repository is archived.")
# Make sure the repository is not in the blacklist.
if is_removed(repository.data.full_name):
repository.validate.errors.append("Repository is in the blacklist.")
raise HacsException("Repository is in the blacklist.")
# Get releases.
try:
releases = await get_releases(
repository.repository_object,
repository.status.show_beta,
hacs.configuration.release_limit,
)
if releases:
repository.releases.releases = True
repository.releases.objects = releases
repository.releases.published_tags = [
x.tag_name for x in releases if not x.draft
]
repository.versions.available = next(iter(releases)).tag_name
for release in releases:
if release.tag_name == repository.ref:
assets = release.assets
if assets:
downloads = next(iter(assets)).attributes.get("download_count")
repository.releases.downloads = downloads
except (AIOGitHubException, HacsException):
repository.releases.releases = False
repository.ref = version_to_install(repository)
repository.logger.debug(
f"Running checks against {repository.ref.replace('tags/', '')}"
)
try:
repository.tree = await get_tree(repository.repository_object, repository.ref)
if not repository.tree:
raise HacsException("No files in tree")
repository.treefiles = []
for treefile in repository.tree:
repository.treefiles.append(treefile.full_path)
except (AIOGitHubException, HacsException) as exception:
if not hacs.system.status.startup:
repository.logger.error(exception)
raise HacsException(exception)

106
config/custom_components/hacs/http.py Executable file → Normal file
View File

@@ -1,13 +1,14 @@
"""HACS http endpoints.""" """HACS http endpoints."""
import os import os
from integrationhelper import Logger
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from aiohttp import web from aiohttp import web
from hacs_frontend import locate_gz, locate_debug_gz from hacs_frontend import locate_gz, locate_debug_gz
from .hacsbase import Hacs from custom_components.hacs.globals import get_hacs
class HacsFrontend(HomeAssistantView, Hacs): class HacsFrontend(HomeAssistantView):
"""Base View Class for HACS.""" """Base View Class for HACS."""
requires_auth = False requires_auth = False
@@ -16,50 +17,67 @@ class HacsFrontend(HomeAssistantView, Hacs):
async def get(self, request, requested_file): # pylint: disable=unused-argument async def get(self, request, requested_file): # pylint: disable=unused-argument
"""Handle HACS Web requests.""" """Handle HACS Web requests."""
return await get_file_response(requested_file)
if requested_file.startswith("frontend-"):
if self.configuration.debug:
servefile = await self.hass.async_add_executor_job(locate_debug_gz)
self.logger.debug("Serving DEBUG frontend")
else:
servefile = await self.hass.async_add_executor_job(locate_gz)
if os.path.exists(servefile):
return web.FileResponse(servefile)
elif requested_file == "iconset.js":
return web.FileResponse(
f"{self.system.config_path}/custom_components/hacs/iconset.js"
)
try:
if requested_file.startswith("themes"):
file = f"{self.system.config_path}/{requested_file}"
else:
file = f"{self.system.config_path}/www/community/{requested_file}"
# Serve .gz if it exist
if os.path.exists(file + ".gz"):
file += ".gz"
if os.path.exists(file):
self.logger.debug("Serving {} from {}".format(requested_file, file))
response = web.FileResponse(file)
response.headers["Cache-Control"] = "max-age=0, must-revalidate"
return response
else:
self.logger.error(f"Tried to serve up '{file}' but it does not exist")
except Exception as error: # pylint: disable=broad-except
self.logger.debug(
"there was an issue trying to serve {} - {}".format(
requested_file, error
)
)
return web.Response(status=404)
class HacsPluginViewLegacy(HacsFrontend): class HacsPluginViewLegacy(HacsFrontend):
"""Alias for legacy, remove with 2.0""" """Alias for legacy, remove with 1.0"""
name = "community_plugin"
url = r"/community_plugin/{requested_file:.+}" url = r"/community_plugin/{requested_file:.+}"
async def get(self, request, requested_file): # pylint: disable=unused-argument
"""DEPRECATED."""
hacs = get_hacs()
if hacs.system.ha_version.split(".")[1] >= "107":
logger = Logger("hacs.deprecated")
logger.warning(
"The '/community_plugin/*' is deprecated and will be removed in an upcomming version of HACS, it has been replaced by '/hacsfiles/*', if you use the UI to manage your lovelace configuration, you can update this by going to the settings tab in HACS, if you use YAML to manage your lovelace configuration, you manually need to replace the URL in your resources."
)
return await get_file_response(requested_file)
async def get_file_response(requested_file):
"""Get file."""
hacs = get_hacs()
if requested_file.startswith("frontend-"):
if hacs.configuration.debug:
servefile = await hacs.hass.async_add_executor_job(locate_debug_gz)
hacs.logger.debug("Serving DEBUG frontend")
else:
servefile = await hacs.hass.async_add_executor_job(locate_gz)
if os.path.exists(servefile):
return web.FileResponse(servefile)
elif requested_file == "iconset.js":
return web.FileResponse(
f"{hacs.system.config_path}/custom_components/hacs/iconset.js"
)
try:
if requested_file.startswith("themes"):
file = f"{hacs.system.config_path}/{requested_file}"
else:
file = f"{hacs.system.config_path}/www/community/{requested_file}"
# Serve .gz if it exist
if os.path.exists(file + ".gz"):
file += ".gz"
if os.path.exists(file):
hacs.logger.debug("Serving {} from {}".format(requested_file, file))
response = web.FileResponse(file)
response.headers["Cache-Control"] = "no-store, max-age=0"
response.headers["Pragma"] = "no-store"
return response
else:
hacs.logger.error(f"Tried to serve up '{file}' but it does not exist")
except Exception as error: # pylint: disable=broad-except
hacs.logger.debug(
"there was an issue trying to serve {} - {}".format(requested_file, error)
)
return web.Response(status=404)

0
config/custom_components/hacs/iconset.js Executable file → Normal file
View File

5
config/custom_components/hacs/manifest.json Executable file → Normal file
View File

@@ -9,14 +9,15 @@
"persistent_notification", "persistent_notification",
"lovelace" "lovelace"
], ],
"documentation": "https://hacs.xyz", "documentation": "https://hacs.xyz/docs/configuration/start",
"domain": "hacs", "domain": "hacs",
"issues": "https://hacs.xyz/docs/issues",
"name": "HACS (Home Assistant Community Store)", "name": "HACS (Home Assistant Community Store)",
"requirements": [ "requirements": [
"aiofiles==0.4.0", "aiofiles==0.4.0",
"aiogithubapi==0.5.0", "aiogithubapi==0.5.0",
"backoff==1.10.0", "backoff==1.10.0",
"hacs_frontend==20200212060803", "hacs_frontend==20200309184730",
"integrationhelper==0.2.2", "integrationhelper==0.2.2",
"semantic_version==2.8.4" "semantic_version==2.8.4"
] ]

20
config/custom_components/hacs/repositories/__init__.py Executable file → Normal file
View File

@@ -1,6 +1,16 @@
"""Initialize repositories.""" """Initialize repositories."""
from .theme import HacsTheme from custom_components.hacs.repositories.theme import HacsTheme
from .integration import HacsIntegration from custom_components.hacs.repositories.integration import HacsIntegration
from .python_script import HacsPythonScript from custom_components.hacs.repositories.python_script import HacsPythonScript
from .appdaemon import HacsAppdaemon from custom_components.hacs.repositories.appdaemon import HacsAppdaemon
from .plugin import HacsPlugin from custom_components.hacs.repositories.netdaemon import HacsNetdaemon
from custom_components.hacs.repositories.plugin import HacsPlugin
RERPOSITORY_CLASSES = {
"theme": HacsTheme,
"integration": HacsIntegration,
"python_script": HacsPythonScript,
"appdaemon": HacsAppdaemon,
"netdaemon": HacsNetdaemon,
"plugin": HacsPlugin,
}

30
config/custom_components/hacs/repositories/appdaemon.py Executable file → Normal file
View File

@@ -1,27 +1,27 @@
"""Class for appdaemon apps in HACS.""" """Class for appdaemon apps in HACS."""
from aiogithubapi import AIOGitHubException from aiogithubapi import AIOGitHubException
from .repository import HacsRepository, register_repository_class from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException from ..hacsbase.exceptions import HacsException
@register_repository_class
class HacsAppdaemon(HacsRepository): class HacsAppdaemon(HacsRepository):
"""Appdaemon apps in HACS.""" """Appdaemon apps in HACS."""
category = "appdaemon"
def __init__(self, full_name): def __init__(self, full_name):
"""Initialize.""" """Initialize."""
super().__init__() super().__init__()
self.information.full_name = full_name self.data.full_name = full_name
self.information.category = self.category self.data.category = "appdaemon"
self.content.path.local = self.localpath self.content.path.local = self.localpath
self.content.path.remote = "apps" self.content.path.remote = "apps"
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
@property @property
def localpath(self): def localpath(self):
"""Return localpath.""" """Return localpath."""
return f"{self.system.config_path}/appdaemon/apps/{self.information.name}" return f"{self.hacs.system.config_path}/appdaemon/apps/{self.data.name}"
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
@@ -39,19 +39,14 @@ class HacsAppdaemon(HacsRepository):
self.validate.errors.append("Repostitory structure not compliant") self.validate.errors.append("Repostitory structure not compliant")
self.content.path.remote = addir[0].path self.content.path.remote = addir[0].path
self.information.name = addir[0].name
self.content.objects = await self.repository_object.get_contents( self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref self.content.path.remote, self.ref
) )
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Handle potential errors # Handle potential errors
if self.validate.errors: if self.validate.errors:
for error in self.validate.errors: for error in self.validate.errors:
if not self.system.status.startup: if not self.hacs.system.status.startup:
self.logger.error(error) self.logger.error(error)
return self.validate.success return self.validate.success
@@ -68,13 +63,13 @@ class HacsAppdaemon(HacsRepository):
async def update_repository(self): async def update_repository(self):
"""Update.""" """Update."""
if self.github.ratelimits.remaining == 0: if self.hacs.github.ratelimits.remaining == 0:
return return
await self.common_update() await self.common_update()
# Get appdaemon objects. # Get appdaemon objects.
if self.repository_manifest: if self.repository_manifest:
if self.repository_manifest.content_in_root: if self.data.content_in_root:
self.content.path.remote = "" self.content.path.remote = ""
if self.content.path.remote == "apps": if self.content.path.remote == "apps":
@@ -82,14 +77,9 @@ class HacsAppdaemon(HacsRepository):
self.content.path.remote, self.ref self.content.path.remote, self.ref
) )
self.content.path.remote = addir[0].path self.content.path.remote = addir[0].path
self.information.name = addir[0].name
self.content.objects = await self.repository_object.get_contents( self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref self.content.path.remote, self.ref
) )
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Set local path # Set local path
self.content.path.local = self.localpath self.content.path.local = self.localpath

134
config/custom_components/hacs/repositories/integration.py Executable file → Normal file
View File

@@ -1,80 +1,56 @@
"""Class for integrations in HACS.""" """Class for integrations in HACS."""
import json from integrationhelper import Logger
from aiogithubapi import AIOGitHubException
from homeassistant.loader import async_get_custom_components from homeassistant.loader import async_get_custom_components
from .repository import HacsRepository, register_repository_class
from ..hacsbase.exceptions import HacsException from custom_components.hacs.hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.filters import get_first_directory_in_directory
from custom_components.hacs.helpers.information import get_integration_manifest
from custom_components.hacs.repositories.repository import HacsRepository
@register_repository_class
class HacsIntegration(HacsRepository): class HacsIntegration(HacsRepository):
"""Integrations in HACS.""" """Integrations in HACS."""
category = "integration"
def __init__(self, full_name): def __init__(self, full_name):
"""Initialize.""" """Initialize."""
super().__init__() super().__init__()
self.information.full_name = full_name self.data.full_name = full_name
self.information.category = self.category self.data.category = "integration"
self.domain = None
self.content.path.remote = "custom_components" self.content.path.remote = "custom_components"
self.content.path.local = self.localpath self.content.path.local = self.localpath
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
@property @property
def localpath(self): def localpath(self):
"""Return localpath.""" """Return localpath."""
return f"{self.system.config_path}/custom_components/{self.domain}" return f"{self.hacs.system.config_path}/custom_components/{self.data.domain}"
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
await self.common_validate() await self.common_validate()
# Attach repository
if self.repository_object is None:
self.repository_object = await self.github.get_repo(
self.information.full_name
)
# Custom step 1: Validate content. # Custom step 1: Validate content.
if self.repository_manifest: if self.data.content_in_root:
if self.repository_manifest.content_in_root: self.content.path.remote = ""
self.content.path.remote = ""
if self.content.path.remote == "custom_components": if self.content.path.remote == "custom_components":
try: name = get_first_directory_in_directory(self.tree, "custom_components")
ccdir = await self.repository_object.get_contents( if name is None:
self.content.path.remote, self.ref
)
except AIOGitHubException:
raise HacsException( raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
) )
self.content.path.remote = f"custom_components/{name}"
for item in ccdir or []: try:
if item.type == "dir": await get_integration_manifest(self)
self.content.path.remote = item.path except HacsException as exception:
break self.logger.error(exception)
if self.repository_manifest.zip_release:
self.content.objects = self.releases.last_release_object.assets
else:
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects or []:
self.content.files.append(filename.name)
if not await self.get_manifest():
self.validate.errors.append("Missing manifest file.")
# Handle potential errors # Handle potential errors
if self.validate.errors: if self.validate.errors:
for error in self.validate.errors: for error in self.validate.errors:
if not self.system.status.startup: if not self.hacs.system.status.startup:
self.logger.error(error) self.logger.error(error)
return self.validate.success return self.validate.success
@@ -86,46 +62,26 @@ class HacsIntegration(HacsRepository):
# Run common registration steps. # Run common registration steps.
await self.common_registration() await self.common_registration()
# Get the content of the manifest file.
await self.get_manifest()
# Set local path # Set local path
self.content.path.local = self.localpath self.content.path.local = self.localpath
async def update_repository(self): async def update_repository(self):
"""Update.""" """Update."""
if self.github.ratelimits.remaining == 0: if self.hacs.github.ratelimits.remaining == 0:
return return
await self.common_update() await self.common_update()
# Get integration objects. if self.data.content_in_root:
self.content.path.remote = ""
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "custom_components": if self.content.path.remote == "custom_components":
ccdir = await self.repository_object.get_contents( name = get_first_directory_in_directory(self.tree, "custom_components")
self.content.path.remote, self.ref self.content.path.remote = f"custom_components/{name}"
)
if not isinstance(ccdir, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.path.remote = ccdir[0].path
try: try:
self.content.objects = await self.repository_object.get_contents( await get_integration_manifest(self)
self.content.path.remote, self.ref except HacsException as exception:
) self.logger.error(exception)
except AIOGitHubException:
return
self.content.files = []
if isinstance(self.content.objects, list):
for filename in self.content.objects or []:
self.content.files.append(filename.name)
await self.get_manifest()
# Set local path # Set local path
self.content.path.local = self.localpath self.content.path.local = self.localpath
@@ -133,33 +89,5 @@ class HacsIntegration(HacsRepository):
async def reload_custom_components(self): async def reload_custom_components(self):
"""Reload custom_components (and config flows)in HA.""" """Reload custom_components (and config flows)in HA."""
self.logger.info("Reloading custom_component cache") self.logger.info("Reloading custom_component cache")
del self.hass.data["custom_components"] del self.hacs.hass.data["custom_components"]
await async_get_custom_components(self.hass) await async_get_custom_components(self.hacs.hass)
async def get_manifest(self):
"""Get info from the manifest file."""
manifest_path = f"{self.content.path.remote}/manifest.json"
try:
manifest = await self.repository_object.get_contents(
manifest_path, self.ref
)
manifest = json.loads(manifest.content)
except Exception: # pylint: disable=broad-except
return False
if manifest:
try:
self.manifest = manifest
self.information.authors = manifest["codeowners"]
self.domain = manifest["domain"]
self.information.name = manifest["name"]
self.information.homeassistant_version = manifest.get("homeassistant")
# Set local path
self.content.path.local = self.localpath
return True
except KeyError as exception:
raise HacsException(
f"Missing expected key {exception} in 'manifest.json'"
)
return False

0
config/custom_components/hacs/repositories/manifest.py Executable file → Normal file
View File

View File

@@ -0,0 +1,90 @@
"""Class for netdaemon apps in HACS."""
from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.filters import get_first_directory_in_directory
class HacsNetdaemon(HacsRepository):
"""Netdaemon apps in HACS."""
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.data.full_name = full_name
self.data.category = "netdaemon"
self.content.path.local = self.localpath
self.content.path.remote = "apps"
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
@property
def localpath(self):
"""Return localpath."""
return f"{self.hacs.system.config_path}/netdaemon/apps/{self.data.name}"
async def validate_repository(self):
"""Validate."""
await self.common_validate()
# Custom step 1: Validate content.
if self.repository_manifest:
if self.data.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "apps":
self.data.domain = get_first_directory_in_directory(
self.tree, self.content.path.remote
)
self.content.path.remote = f"apps/{self.data.name}"
compliant = False
for treefile in self.treefiles:
if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(
".cs"
):
compliant = True
break
if not compliant:
raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.hacs.system.status.startup:
self.logger.error(error)
return self.validate.success
async def registration(self):
"""Registration."""
if not await self.validate_repository():
return False
# Run common registration steps.
await self.common_registration()
# Set local path
self.content.path.local = self.localpath
async def update_repository(self):
"""Update."""
if self.hacs.github.ratelimits.remaining == 0:
return
await self.common_update()
# Get appdaemon objects.
if self.repository_manifest:
if self.data.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "apps":
self.data.domain = get_first_directory_in_directory(
self.tree, self.content.path.remote
)
self.content.path.remote = f"apps/{self.data.name}"
# Set local path
self.content.path.local = self.localpath

95
config/custom_components/hacs/repositories/plugin.py Executable file → Normal file
View File

@@ -1,26 +1,27 @@
"""Class for plugins in HACS.""" """Class for plugins in HACS."""
import json import json
from aiogithubapi import AIOGitHubException from integrationhelper import Logger
from .repository import HacsRepository, register_repository_class
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException from ..hacsbase.exceptions import HacsException
from custom_components.hacs.helpers.information import find_file_name
@register_repository_class
class HacsPlugin(HacsRepository): class HacsPlugin(HacsRepository):
"""Plugins in HACS.""" """Plugins in HACS."""
category = "plugin"
def __init__(self, full_name): def __init__(self, full_name):
"""Initialize.""" """Initialize."""
super().__init__() super().__init__()
self.information.full_name = full_name self.data.full_name = full_name
self.information.category = self.category self.data.file_name = None
self.information.file_name = None self.data.category = "plugin"
self.information.javascript_type = None self.information.javascript_type = None
self.content.path.local = ( self.content.path.local = (
f"{self.system.config_path}/www/community/{full_name.split('/')[-1]}" f"{self.hacs.system.config_path}/www/community/{full_name.split('/')[-1]}"
) )
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
@@ -28,7 +29,7 @@ class HacsPlugin(HacsRepository):
await self.common_validate() await self.common_validate()
# Custom step 1: Validate content. # Custom step 1: Validate content.
await self.get_plugin_location() find_file_name(self)
if self.content.path.remote is None: if self.content.path.remote is None:
raise HacsException( raise HacsException(
@@ -38,14 +39,10 @@ class HacsPlugin(HacsRepository):
if self.content.path.remote == "release": if self.content.path.remote == "release":
self.content.single = True self.content.single = True
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Handle potential errors # Handle potential errors
if self.validate.errors: if self.validate.errors:
for error in self.validate.errors: for error in self.validate.errors:
if not self.system.status.startup: if not self.hacs.system.status.startup:
self.logger.error(error) self.logger.error(error)
return self.validate.success return self.validate.success
@@ -59,13 +56,13 @@ class HacsPlugin(HacsRepository):
async def update_repository(self): async def update_repository(self):
"""Update.""" """Update."""
if self.github.ratelimits.remaining == 0: if self.hacs.github.ratelimits.remaining == 0:
return return
# Run common update steps. # Run common update steps.
await self.common_update() await self.common_update()
# Get plugin objects. # Get plugin objects.
await self.get_plugin_location() find_file_name(self)
# Get JS type # Get JS type
await self.parse_readme_for_jstype() await self.parse_readme_for_jstype()
@@ -76,68 +73,6 @@ class HacsPlugin(HacsRepository):
if self.content.path.remote == "release": if self.content.path.remote == "release":
self.content.single = True self.content.single = True
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
async def get_plugin_location(self):
"""Get plugin location."""
if self.content.path.remote is not None:
return
possible_locations = ["dist", "release", ""]
if self.repository_manifest:
if self.repository_manifest.content_in_root:
possible_locations = [""]
for location in possible_locations:
if self.content.path.remote is not None:
continue
try:
objects = []
files = []
if location != "release":
try:
objects = await self.repository_object.get_contents(
location, self.ref
)
except AIOGitHubException:
continue
else:
await self.get_releases()
if self.releases.releases:
if self.releases.last_release_object.assets is not None:
objects = self.releases.last_release_object.assets
for item in objects:
if item.name.endswith(".js"):
files.append(item.name)
# Handler for plug requirement 3
valid_filenames = [
f"{self.information.name.replace('lovelace-', '')}.js",
f"{self.information.name}.js",
f"{self.information.name}.umd.js",
f"{self.information.name}-bundle.js",
]
if self.repository_manifest:
if self.repository_manifest.filename:
valid_filenames.append(self.repository_manifest.filename)
for name in valid_filenames:
if name in files:
# YES! We got it!
self.information.file_name = name
self.content.path.remote = location
self.content.objects = objects
self.content.files = files
break
except SystemError:
pass
async def get_package_content(self): async def get_package_content(self):
"""Get package content.""" """Get package content."""
try: try:
@@ -145,7 +80,7 @@ class HacsPlugin(HacsRepository):
package = json.loads(package.content) package = json.loads(package.content)
if package: if package:
self.information.authors = package["author"] self.data.authors = package["author"]
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
pass pass

View File

@@ -1,10 +1,11 @@
"""Class for python_scripts in HACS.""" """Class for python_scripts in HACS."""
from aiogithubapi import AIOGitHubException from integrationhelper import Logger
from .repository import HacsRepository, register_repository_class
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException from ..hacsbase.exceptions import HacsException
from ..helpers.information import find_file_name
@register_repository_class
class HacsPythonScript(HacsRepository): class HacsPythonScript(HacsRepository):
"""python_scripts in HACS.""" """python_scripts in HACS."""
@@ -13,11 +14,12 @@ class HacsPythonScript(HacsRepository):
def __init__(self, full_name): def __init__(self, full_name):
"""Initialize.""" """Initialize."""
super().__init__() super().__init__()
self.information.full_name = full_name self.data.full_name = full_name
self.information.category = self.category self.data.category = "python_script"
self.content.path.remote = "python_scripts" self.content.path.remote = "python_scripts"
self.content.path.local = f"{self.system.config_path}/python_scripts" self.content.path.local = f"{self.hacs.system.config_path}/python_scripts"
self.content.single = True self.content.single = True
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
@@ -25,26 +27,25 @@ class HacsPythonScript(HacsRepository):
await self.common_validate() await self.common_validate()
# Custom step 1: Validate content. # Custom step 1: Validate content.
try: if self.data.content_in_root:
self.content.objects = await self.repository_object.get_contents( self.content.path.remote = ""
self.content.path.remote, self.ref
) compliant = False
except AIOGitHubException: for treefile in self.treefiles:
if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(
".py"
):
compliant = True
break
if not compliant:
raise HacsException( raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
) )
if not isinstance(self.content.objects, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Handle potential errors # Handle potential errors
if self.validate.errors: if self.validate.errors:
for error in self.validate.errors: for error in self.validate.errors:
if not self.system.status.startup: if not self.hacs.system.status.startup:
self.logger.error(error) self.logger.error(error)
return self.validate.success return self.validate.success
@@ -57,31 +58,30 @@ class HacsPythonScript(HacsRepository):
await self.common_registration() await self.common_registration()
# Set name # Set name
self.information.name = self.content.objects[0].name.replace(".py", "") find_file_name(self)
async def update_repository(self): # lgtm[py/similar-function] async def update_repository(self): # lgtm[py/similar-function]
"""Update.""" """Update."""
if self.github.ratelimits.remaining == 0: if self.hacs.github.ratelimits.remaining == 0:
return return
# Run common update steps. # Run common update steps.
await self.common_update() await self.common_update()
# Get python_script objects. # Get python_script objects.
if self.repository_manifest: if self.data.content_in_root:
if self.repository_manifest.content_in_root: self.content.path.remote = ""
self.content.path.remote = ""
self.content.objects = await self.repository_object.get_contents( compliant = False
self.content.path.remote, self.ref for treefile in self.treefiles:
) if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(
".py"
self.content.files = [] ):
for filename in self.content.objects: compliant = True
self.content.files.append(filename.name) break
if not compliant:
raise HacsException(
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
)
# Update name # Update name
self.information.name = self.content.objects[0].name.replace(".py", "") find_file_name(self)
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)

View File

@@ -0,0 +1,17 @@
"""Object for removed repositories."""
import attr
@attr.s(auto_attribs=True)
class RemovedRepository:
repository: str = None
reason: str = None
link: str = None
removal_type: str = None # archived, not_compliant, critical, dev, broken
acknowledged: bool = False
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if key in self.__dict__:
setattr(self, key, data[key])

307
config/custom_components/hacs/repositories/repository.py Executable file → Normal file
View File

@@ -1,29 +1,27 @@
"""Repository.""" """Repository."""
# pylint: disable=broad-except, bad-continuation, no-member # pylint: disable=broad-except, bad-continuation, no-member
import pathlib
import json import json
import os import os
import tempfile import tempfile
import zipfile import zipfile
from integrationhelper import Validate, Logger from integrationhelper import Validate
from aiogithubapi import AIOGitHubException from aiogithubapi import AIOGitHubException
from .manifest import HacsManifest from .manifest import HacsManifest
from ..helpers.misc import get_repository_name from ..helpers.misc import get_repository_name
from ..hacsbase import Hacs
from ..hacsbase.exceptions import HacsException
from ..hacsbase.backup import Backup
from ..handler.download import async_download_file, async_save_file from ..handler.download import async_download_file, async_save_file
from ..helpers.misc import version_left_higher_then_right from ..helpers.misc import version_left_higher_then_right
from ..helpers.install import install_repository, version_to_install from ..helpers.install import install_repository, version_to_install
from custom_components.hacs.globals import get_hacs
RERPOSITORY_CLASSES = {} from custom_components.hacs.helpers.information import (
get_info_md_content,
get_repository,
def register_repository_class(cls): )
"""Register class.""" from custom_components.hacs.helpers.validate_repository import (
RERPOSITORY_CLASSES[cls.category] = cls common_validate,
return cls common_update_data,
)
from custom_components.hacs.repositories.repositorydata import RepositoryData
class RepositoryVersions: class RepositoryVersions:
@@ -79,6 +77,7 @@ class RepositoryReleases:
published_tags = [] published_tags = []
objects = [] objects = []
releases = False releases = False
downloads = None
class RepositoryPath: class RepositoryPath:
@@ -97,26 +96,25 @@ class RepositoryContent:
single = False single = False
class HacsRepository(Hacs): class HacsRepository:
"""HacsRepository.""" """HacsRepository."""
def __init__(self): def __init__(self):
"""Set up HacsRepository.""" """Set up HacsRepository."""
self.hacs = get_hacs()
self.data = {} self.data = RepositoryData()
self.content = RepositoryContent() self.content = RepositoryContent()
self.content.path = RepositoryPath() self.content.path = RepositoryPath()
self.information = RepositoryInformation() self.information = RepositoryInformation()
self.repository_object = None self.repository_object = None
self.status = RepositoryStatus() self.status = RepositoryStatus()
self.state = None self.state = None
self.manifest = {} self.integration_manifest = {}
self.repository_manifest = HacsManifest.from_dict({}) self.repository_manifest = HacsManifest.from_dict({})
self.validate = Validate() self.validate = Validate()
self.releases = RepositoryReleases() self.releases = RepositoryReleases()
self.versions = RepositoryVersions() self.versions = RepositoryVersions()
self.pending_restart = False self.pending_restart = False
self.logger = None
self.tree = [] self.tree = []
self.treefiles = [] self.treefiles = []
self.ref = None self.ref = None
@@ -126,7 +124,7 @@ class HacsRepository(Hacs):
"""Return pending upgrade.""" """Return pending upgrade."""
if self.status.installed: if self.status.installed:
if self.status.selected_tag is not None: if self.status.selected_tag is not None:
if self.status.selected_tag == self.information.default_branch: if self.status.selected_tag == self.data.default_branch:
if self.versions.installed_commit != self.versions.available_commit: if self.versions.installed_commit != self.versions.available_commit:
return True return True
return False return False
@@ -137,23 +135,20 @@ class HacsRepository(Hacs):
@property @property
def config_flow(self): def config_flow(self):
"""Return bool if integration has config_flow.""" """Return bool if integration has config_flow."""
if self.manifest: if self.integration_manifest:
if self.information.full_name == "hacs/integration": if self.data.full_name == "hacs/integration":
return False return False
return self.manifest.get("config_flow", False) return self.integration_manifest.get("config_flow", False)
return False return False
@property @property
def custom(self): def custom(self):
"""Return flag if the repository is custom.""" """Return flag if the repository is custom."""
if self.information.full_name.split("/")[0] in [ if self.data.full_name.split("/")[0] in ["custom-components", "custom-cards"]:
"custom-components",
"custom-cards",
]:
return False return False
if self.information.full_name in self.common.default: if self.data.full_name.lower() in [x.lower() for x in self.hacs.common.default]:
return False return False
if self.information.full_name == "hacs/integration": if self.data.full_name == "hacs/integration":
return False return False
return True return True
@@ -164,24 +159,21 @@ class HacsRepository(Hacs):
if self.information.homeassistant_version is not None: if self.information.homeassistant_version is not None:
target = self.information.homeassistant_version target = self.information.homeassistant_version
if self.repository_manifest is not None: if self.repository_manifest is not None:
if self.repository_manifest.homeassistant is not None: if self.data.homeassistant is not None:
target = self.repository_manifest.homeassistant target = self.data.homeassistant
if target is not None: if target is not None:
if self.releases.releases: if self.releases.releases:
if not version_left_higher_then_right(self.system.ha_version, target): if not version_left_higher_then_right(
self.hacs.system.ha_version, target
):
return False return False
return True return True
@property @property
def display_name(self): def display_name(self):
"""Return display name.""" """Return display name."""
return get_repository_name( return get_repository_name(self)
self.repository_manifest,
self.information.name,
self.information.category,
self.manifest,
)
@property @property
def display_status(self): def display_status(self):
@@ -257,127 +249,41 @@ class HacsRepository(Hacs):
async def common_validate(self): async def common_validate(self):
"""Common validation steps of the repository.""" """Common validation steps of the repository."""
# Attach helpers await common_validate(self)
self.validate.errors = []
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
if self.ref is None:
self.ref = version_to_install(self)
# Step 1: Make sure the repository exist.
self.logger.debug("Checking repository.")
try:
self.repository_object = await self.github.get_repo(
self.information.full_name
)
self.data = self.repository_object.attributes
except Exception as exception: # Gotta Catch 'Em All
if not self.system.status.startup:
self.logger.error(exception)
self.validate.errors.append("Repository does not exist.")
return
if not self.tree:
self.tree = await self.repository_object.get_tree(self.ref)
self.treefiles = []
for treefile in self.tree:
self.treefiles.append(treefile.full_path)
# Step 2: Make sure the repository is not archived.
if self.repository_object.archived:
self.validate.errors.append("Repository is archived.")
return
# Step 3: Make sure the repository is not in the blacklist.
if self.information.full_name in self.common.blacklist:
self.validate.errors.append("Repository is in the blacklist.")
return
# Step 4: default branch
self.information.default_branch = self.repository_object.default_branch
# Step 5: Get releases.
await self.get_releases()
# Step 6: Get the content of hacs.json
await self.get_repository_manifest_content()
# Set repository name
self.information.name = self.information.full_name.split("/")[1]
async def common_registration(self): async def common_registration(self):
"""Common registration steps of the repository.""" """Common registration steps of the repository."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
# Attach repository # Attach repository
if self.repository_object is None: if self.repository_object is None:
self.repository_object = await self.github.get_repo( self.repository_object = await get_repository(
self.information.full_name self.hacs.session, self.hacs.configuration.token, self.data.full_name
) )
self.data.update_data(self.repository_object.attributes)
# Set id # Set id
self.information.uid = str(self.repository_object.id) self.information.uid = str(self.data.id)
# Set topics # Set topics
self.information.topics = self.repository_object.topics self.data.topics = self.data.topics
# Set stargazers_count # Set stargazers_count
self.information.stars = self.repository_object.attributes.get( self.data.stargazers_count = self.data.stargazers_count
"stargazers_count", 0
)
# Set description # Set description
if self.repository_object.description: self.data.description = self.data.description
self.information.description = self.repository_object.description
async def common_update(self): async def common_update(self):
"""Common information update steps of the repository.""" """Common information update steps of the repository."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
self.logger.debug("Getting repository information") self.logger.debug("Getting repository information")
# Set ref
if self.ref is None:
self.ref = version_to_install(self)
# Attach repository # Attach repository
self.repository_object = await self.github.get_repo(self.information.full_name) await common_update_data(self)
# Update tree
self.tree = await self.repository_object.get_tree(self.ref)
self.treefiles = []
for treefile in self.tree:
self.treefiles.append(treefile.full_path)
# Update description
if self.repository_object.description:
self.information.description = self.repository_object.description
# Set stargazers_count
self.information.stars = self.repository_object.attributes.get(
"stargazers_count", 0
)
# Update default branch
self.information.default_branch = self.repository_object.default_branch
# Update last updaeted # Update last updaeted
self.information.last_updated = self.repository_object.attributes.get( self.information.last_updated = self.repository_object.attributes.get(
"pushed_at", 0 "pushed_at", 0
) )
# Update topics
self.information.topics = self.repository_object.topics
# Update last available commit # Update last available commit
await self.repository_object.set_last_commit() await self.repository_object.set_last_commit()
self.versions.available_commit = self.repository_object.last_commit self.versions.available_commit = self.repository_object.last_commit
@@ -386,10 +292,7 @@ class HacsRepository(Hacs):
await self.get_repository_manifest_content() await self.get_repository_manifest_content()
# Update "info.md" # Update "info.md"
await self.get_info_md_content() self.information.additional_info = await get_info_md_content(self)
# Update releases
await self.get_releases()
async def install(self): async def install(self):
"""Common installation steps of the repository.""" """Common installation steps of the repository."""
@@ -409,18 +312,17 @@ class HacsRepository(Hacs):
return validate return validate
for content in contents or []: for content in contents or []:
filecontent = await async_download_file(self.hass, content.download_url) filecontent = await async_download_file(content.download_url)
if filecontent is None: if filecontent is None:
validate.errors.append(f"[{content.name}] was not downloaded.") validate.errors.append(f"[{content.name}] was not downloaded.")
continue continue
result = await async_save_file( result = await async_save_file(
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", f"{tempfile.gettempdir()}/{self.data.filename}", filecontent
filecontent,
) )
with zipfile.ZipFile( with zipfile.ZipFile(
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", "r" f"{tempfile.gettempdir()}/{self.data.filename}", "r"
) as zip_file: ) as zip_file:
zip_file.extractall(self.content.path.local) zip_file.extractall(self.content.path.local)
@@ -437,11 +339,13 @@ class HacsRepository(Hacs):
"""Download the content of a directory.""" """Download the content of a directory."""
from custom_components.hacs.helpers.download import download_content from custom_components.hacs.helpers.download import download_content
validate = await download_content(self, validate, local_directory) validate = await download_content(self)
return validate return validate
async def get_repository_manifest_content(self): async def get_repository_manifest_content(self):
"""Get the content of the hacs.json file.""" """Get the content of the hacs.json file."""
if not "hacs.json" in [x.filename for x in self.tree]:
return
if self.ref is None: if self.ref is None:
self.ref = version_to_install(self) self.ref = version_to_install(self)
try: try:
@@ -449,125 +353,44 @@ class HacsRepository(Hacs):
self.repository_manifest = HacsManifest.from_dict( self.repository_manifest = HacsManifest.from_dict(
json.loads(manifest.content) json.loads(manifest.content)
) )
self.data.update_data(json.loads(manifest.content))
except (AIOGitHubException, Exception): # Gotta Catch 'Em All except (AIOGitHubException, Exception): # Gotta Catch 'Em All
pass pass
async def get_info_md_content(self):
"""Get the content of info.md"""
from ..handler.template import render_template
if self.ref is None:
self.ref = version_to_install(self)
info = None
info_files = ["info", "info.md"]
if self.repository_manifest is not None:
if self.repository_manifest.render_readme:
info_files = ["readme", "readme.md"]
try:
root = await self.repository_object.get_contents("", self.ref)
for file in root:
if file.name.lower() in info_files:
info = await self.repository_object.get_contents(
file.name, self.ref
)
break
if info is None:
self.information.additional_info = ""
else:
info = info.content.replace("<svg", "<disabled").replace(
"</svg", "</disabled"
)
self.information.additional_info = render_template(info, self)
except (AIOGitHubException, Exception):
self.information.additional_info = ""
async def get_releases(self):
"""Get repository releases."""
if self.status.show_beta:
self.releases.objects = await self.repository_object.get_releases(
prerelease=True, returnlimit=self.configuration.release_limit
)
else:
self.releases.objects = await self.repository_object.get_releases(
prerelease=False, returnlimit=self.configuration.release_limit
)
if not self.releases.objects:
return
self.releases.releases = True
self.releases.published_tags = []
for release in self.releases.objects:
self.releases.published_tags.append(release.tag_name)
self.releases.last_release_object = self.releases.objects[0]
if self.status.selected_tag is not None:
if self.status.selected_tag != self.information.default_branch:
for release in self.releases.objects:
if release.tag_name == self.status.selected_tag:
self.releases.last_release_object = release
break
if self.releases.last_release_object.assets:
self.releases.last_release_object_downloads = self.releases.last_release_object.assets[
0
].attributes.get(
"download_count"
)
self.versions.available = self.releases.objects[0].tag_name
def remove(self): def remove(self):
"""Run remove tasks.""" """Run remove tasks."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
self.logger.info("Starting removal") self.logger.info("Starting removal")
if self.information.uid in self.common.installed: if self.information.uid in self.hacs.common.installed:
self.common.installed.remove(self.information.uid) self.hacs.common.installed.remove(self.information.uid)
for repository in self.repositories: for repository in self.hacs.repositories:
if repository.information.uid == self.information.uid: if repository.information.uid == self.information.uid:
self.repositories.remove(repository) self.hacs.repositories.remove(repository)
async def uninstall(self): async def uninstall(self):
"""Run uninstall tasks.""" """Run uninstall tasks."""
# Attach logger
if self.logger is None:
self.logger = Logger(
f"hacs.repository.{self.information.category}.{self.information.full_name}"
)
self.logger.info("Uninstalling") self.logger.info("Uninstalling")
await self.remove_local_directory() await self.remove_local_directory()
self.status.installed = False self.status.installed = False
if self.information.category == "integration": if self.data.category == "integration":
if self.config_flow: if self.config_flow:
await self.reload_custom_components() await self.reload_custom_components()
else: else:
self.pending_restart = True self.pending_restart = True
elif self.information.category == "theme": elif self.data.category == "theme":
try: try:
await self.hass.services.async_call("frontend", "reload_themes", {}) await self.hacs.hass.services.async_call(
"frontend", "reload_themes", {}
)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
pass pass
if self.information.full_name in self.common.installed: if self.data.full_name in self.hacs.common.installed:
self.common.installed.remove(self.information.full_name) self.hacs.common.installed.remove(self.data.full_name)
self.versions.installed = None self.versions.installed = None
self.versions.installed_commit = None self.versions.installed_commit = None
self.hass.bus.async_fire( self.hacs.hass.bus.async_fire(
"hacs/repository", "hacs/repository",
{ {"id": 1337, "action": "uninstall", "repository": self.data.full_name},
"id": 1337,
"action": "uninstall",
"repository": self.information.full_name,
},
) )
async def remove_local_directory(self): async def remove_local_directory(self):
@@ -576,13 +399,11 @@ class HacsRepository(Hacs):
from asyncio import sleep from asyncio import sleep
try: try:
if self.information.category == "python_script": if self.data.category == "python_script":
local_path = "{}/{}.py".format( local_path = "{}/{}.py".format(self.content.path.local, self.data.name)
self.content.path.local, self.information.name elif self.data.category == "theme":
)
elif self.information.category == "theme":
local_path = "{}/{}.yaml".format( local_path = "{}/{}.yaml".format(
self.content.path.local, self.information.name self.content.path.local, self.data.name
) )
else: else:
local_path = self.content.path.local local_path = self.content.path.local
@@ -590,7 +411,7 @@ class HacsRepository(Hacs):
if os.path.exists(local_path): if os.path.exists(local_path):
self.logger.debug(f"Removing {local_path}") self.logger.debug(f"Removing {local_path}")
if self.information.category in ["python_script", "theme"]: if self.data.category in ["python_script", "theme"]:
os.remove(local_path) os.remove(local_path)
else: else:
shutil.rmtree(local_path) shutil.rmtree(local_path)

View File

@@ -0,0 +1,82 @@
"""Repository data."""
from datetime import datetime
from typing import List
import attr
@attr.s(auto_attribs=True)
class RepositoryData:
"""RepositoryData class."""
id: int = 0
full_name: str = ""
pushed_at: str = ""
category: str = ""
archived: bool = False
description: str = ""
manifest_name: str = None
topics: List[str] = []
fork: bool = False
domain: str = ""
default_branch: str = None
stargazers_count: int = 0
last_commit: str = ""
file_name: str = ""
content_in_root: bool = False
zip_release: bool = False
filename: str = ""
render_readme: bool = False
hide_default_branch: bool = False
domains: List[str] = []
country: List[str] = []
authors: List[str] = []
homeassistant: str = None # Minimum Home Assistant version
hacs: str = None # Minimum HACS version
persistent_directory: str = None
iot_class: str = None
@property
def name(self):
"""Return the name."""
if self.category in ["integration", "netdaemon"]:
return self.domain
return self.full_name.split("/")[-1]
def to_json(self):
"""Export to json."""
return self.__dict__
@staticmethod
def create_from_dict(source: dict):
"""Set attributes from dicts."""
data = RepositoryData()
for key in source:
if key in data.__dict__:
if key == "pushed_at":
setattr(
data, key, datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%SZ")
)
elif key == "county":
if isinstance(source[key], str):
setattr(data, key, [source[key]])
else:
setattr(data, key, source[key])
else:
setattr(data, key, source[key])
return data
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if key in self.__dict__:
if key == "pushed_at":
setattr(
self, key, datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%SZ")
)
elif key == "county":
if isinstance(data[key], str):
setattr(self, key, [data[key]])
else:
setattr(self, key, data[key])
else:
setattr(self, key, data[key])

72
config/custom_components/hacs/repositories/theme.py Executable file → Normal file
View File

@@ -1,23 +1,22 @@
"""Class for themes in HACS.""" """Class for themes in HACS."""
from .repository import HacsRepository, register_repository_class from integrationhelper import Logger
from .repository import HacsRepository
from ..hacsbase.exceptions import HacsException from ..hacsbase.exceptions import HacsException
from ..helpers.filters import filter_content_return_one_of_type, find_first_of_filetype from ..helpers.information import find_file_name
@register_repository_class
class HacsTheme(HacsRepository): class HacsTheme(HacsRepository):
"""Themes in HACS.""" """Themes in HACS."""
category = "theme"
def __init__(self, full_name): def __init__(self, full_name):
"""Initialize.""" """Initialize."""
super().__init__() super().__init__()
self.information.full_name = full_name self.data.full_name = full_name
self.information.category = self.category self.data.category = "theme"
self.content.path.remote = "themes" self.content.path.remote = "themes"
self.content.path.local = f"{self.system.config_path}/themes" self.content.path.local = f"{self.hacs.system.config_path}/themes/"
self.content.single = False self.content.single = False
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
async def validate_repository(self): async def validate_repository(self):
"""Validate.""" """Validate."""
@@ -27,7 +26,6 @@ class HacsTheme(HacsRepository):
# Custom step 1: Validate content. # Custom step 1: Validate content.
compliant = False compliant = False
for treefile in self.treefiles: for treefile in self.treefiles:
self.logger.debug(treefile)
if treefile.startswith("themes/") and treefile.endswith(".yaml"): if treefile.startswith("themes/") and treefile.endswith(".yaml"):
compliant = True compliant = True
break break
@@ -36,25 +34,13 @@ class HacsTheme(HacsRepository):
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
) )
if self.repository_manifest: if self.data.content_in_root:
if self.repository_manifest.content_in_root: self.content.path.remote = ""
self.content.path.remote = ""
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
if not isinstance(self.content.objects, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.files = filter_content_return_one_of_type(
self.treefiles, "themes", "yaml"
)
# Handle potential errors # Handle potential errors
if self.validate.errors: if self.validate.errors:
for error in self.validate.errors: for error in self.validate.errors:
if not self.system.status.startup: if not self.hacs.system.status.startup:
self.logger.error(error) self.logger.error(error)
return self.validate.success return self.validate.success
@@ -67,44 +53,20 @@ class HacsTheme(HacsRepository):
await self.common_registration() await self.common_registration()
# Set name # Set name
if self.repository_manifest.filename is not None: find_file_name(self)
self.information.file_name = self.repository_manifest.filename self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"
else:
self.information.file_name = find_first_of_filetype(
self.content.files, "yaml"
).split("/")[-1]
self.information.name = self.information.file_name.replace(".yaml", "")
self.content.path.local = (
f"{self.system.config_path}/themes/{self.information.name}"
)
async def update_repository(self): # lgtm[py/similar-function] async def update_repository(self): # lgtm[py/similar-function]
"""Update.""" """Update."""
if self.github.ratelimits.remaining == 0: if self.hacs.github.ratelimits.remaining == 0:
return return
# Run common update steps. # Run common update steps.
await self.common_update() await self.common_update()
# Get theme objects. # Get theme objects.
if self.repository_manifest: if self.data.content_in_root:
if self.repository_manifest.content_in_root: self.content.path.remote = ""
self.content.path.remote = ""
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = filter_content_return_one_of_type(
self.treefiles, "themes", "yaml"
)
# Update name # Update name
if self.repository_manifest.filename is not None: find_file_name(self)
self.information.file_name = self.repository_manifest.filename self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"
else:
self.information.file_name = find_first_of_filetype(
self.content.files, "yaml"
).split("/")[-1]
self.information.name = self.information.file_name.replace(".yaml", "")
self.content.path.local = (
f"{self.system.config_path}/themes/{self.information.name}"
)

9
config/custom_components/hacs/sensor.py Executable file → Normal file
View File

@@ -48,7 +48,7 @@ class HACSSensor(HACSDevice):
for repository in hacs.repositories: for repository in hacs.repositories:
if ( if (
repository.pending_upgrade repository.pending_upgrade
and repository.category in hacs.common.categories and repository.data.category in hacs.common.categories
): ):
self.repositories.append(repository) self.repositories.append(repository)
self._state = len(self.repositories) self._state = len(self.repositories)
@@ -87,13 +87,10 @@ class HACSSensor(HACSDevice):
for repository in self.repositories: for repository in self.repositories:
data.append( data.append(
{ {
"name": repository.information.full_name, "name": repository.data.full_name,
"display_name": repository.display_name, "display_name": repository.display_name,
"installed version": repository.display_installed_version, "installed version": repository.display_installed_version,
"available version": repository.display_available_version, "available version": repository.display_available_version,
} }
) )
return { return {"repositories": data}
"repositories": data,
"attribution": "It is expected to see [object Object] here, for more info see https://hacs.xyz/docs/basic/sensor",
}

0
config/custom_components/hacs/services.yaml Executable file → Normal file
View File

59
config/custom_components/hacs/setup.py Executable file → Normal file
View File

@@ -1,51 +1,57 @@
"""Setup functions for HACS.""" """Setup functions for HACS."""
# pylint: disable=bad-continuation # pylint: disable=bad-continuation
from hacs_frontend.version import VERSION as FE_VERSION
from homeassistant.helpers import discovery
from custom_components.hacs.hacsbase.exceptions import HacsException
from custom_components.hacs.const import VERSION, DOMAIN
from custom_components.hacs.globals import get_hacs
from custom_components.hacs.helpers.information import get_repository
from custom_components.hacs.helpers.register_repository import register_repository
async def load_hacs_repository(hacs): async def load_hacs_repository():
"""Load HACS repositroy.""" """Load HACS repositroy."""
from .const import VERSION hacs = get_hacs()
from aiogithubapi import (
AIOGitHubAuthentication,
AIOGitHubException,
AIOGitHubRatelimit,
)
try: try:
repository = hacs().get_by_name("hacs/integration") repository = hacs.get_by_name("hacs/integration")
if repository is None: if repository is None:
await hacs().register_repository("hacs/integration", "integration") await register_repository("hacs/integration", "integration")
repository = hacs().get_by_name("hacs/integration") repository = hacs.get_by_name("hacs/integration")
if repository is None: if repository is None:
raise AIOGitHubException("Unknown error") raise HacsException("Unknown error")
repository.status.installed = True repository.status.installed = True
repository.versions.installed = VERSION repository.versions.installed = VERSION
repository.status.new = False repository.status.new = False
hacs.repo = repository.repository_object hacs.repo = repository.repository_object
hacs.data_repo = await hacs().github.get_repo("hacs/default") hacs.data_repo = await get_repository(
except ( hacs.session, hacs.configuration.token, "hacs/default"
AIOGitHubException, )
AIOGitHubRatelimit, except HacsException as exception:
AIOGitHubAuthentication, if "403" in f"{exception}":
) as exception: hacs.logger.critical("GitHub API is ratelimited, or the token is wrong.")
hacs.logger.critical(f"[{exception}] - Could not load HACS!") else:
hacs.logger.critical(f"[{exception}] - Could not load HACS!")
return False return False
return True return True
def setup_extra_stores(hacs): def setup_extra_stores():
"""Set up extra stores in HACS if enabled in Home Assistant.""" """Set up extra stores in HACS if enabled in Home Assistant."""
hacs = get_hacs()
if "python_script" in hacs.hass.config.components: if "python_script" in hacs.hass.config.components:
hacs.common.categories.append("python_script") if "python_script" not in hacs.common.categories:
hacs.common.categories.append("python_script")
if hacs.hass.services.services.get("frontend", {}).get("reload_themes") is not None: if hacs.hass.services.services.get("frontend", {}).get("reload_themes") is not None:
hacs.common.categories.append("theme") if "theme" not in hacs.common.categories:
hacs.common.categories.append("theme")
def add_sensor(hacs): def add_sensor():
"""Add sensor.""" """Add sensor."""
from .const import DOMAIN hacs = get_hacs()
from homeassistant.helpers import discovery
try: try:
if hacs.configuration.config_type == "yaml": if hacs.configuration.config_type == "yaml":
@@ -64,11 +70,12 @@ def add_sensor(hacs):
pass pass
async def setup_frontend(hacs): async def setup_frontend():
"""Configure the HACS frontend elements.""" """Configure the HACS frontend elements."""
from .http import HacsFrontend, HacsPluginViewLegacy from .http import HacsFrontend, HacsPluginViewLegacy
from .ws_api_handlers import setup_ws_api from .ws_api_handlers import setup_ws_api
from hacs_frontend.version import VERSION as FE_VERSION
hacs = get_hacs()
hacs.hass.http.register_view(HacsFrontend()) hacs.hass.http.register_view(HacsFrontend())
hacs.frontend.version_running = FE_VERSION hacs.frontend.version_running = FE_VERSION

0
config/custom_components/hacs/store.py Executable file → Normal file
View File

126
config/custom_components/hacs/ws_api_handlers.py Executable file → Normal file
View File

@@ -6,10 +6,12 @@ import voluptuous as vol
from aiogithubapi import AIOGitHubException from aiogithubapi import AIOGitHubException
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .hacsbase import Hacs
from .hacsbase.exceptions import HacsException from .hacsbase.exceptions import HacsException
from .store import async_load_from_store, async_save_to_store from .store import async_load_from_store, async_save_to_store
from custom_components.hacs.globals import get_hacs
from custom_components.hacs.helpers.register_repository import register_repository
async def setup_ws_api(hass): async def setup_ws_api(hass):
"""Set up WS API handlers.""" """Set up WS API handlers."""
@@ -34,74 +36,76 @@ async def setup_ws_api(hass):
) )
async def hacs_settings(hass, connection, msg): async def hacs_settings(hass, connection, msg):
"""Handle get media player cover command.""" """Handle get media player cover command."""
hacs = get_hacs()
action = msg["action"] action = msg["action"]
Hacs().logger.debug(f"WS action '{action}'") hacs.logger.debug(f"WS action '{action}'")
if action == "set_fe_grid": if action == "set_fe_grid":
Hacs().configuration.frontend_mode = "Grid" hacs.configuration.frontend_mode = "Grid"
elif action == "onboarding_done": elif action == "onboarding_done":
Hacs().configuration.onboarding_done = True hacs.configuration.onboarding_done = True
elif action == "set_fe_table": elif action == "set_fe_table":
Hacs().configuration.frontend_mode = "Table" hacs.configuration.frontend_mode = "Table"
elif action == "set_fe_compact_true": elif action == "set_fe_compact_true":
Hacs().configuration.frontend_compact = False hacs.configuration.frontend_compact = False
elif action == "set_fe_compact_false": elif action == "set_fe_compact_false":
Hacs().configuration.frontend_compact = True hacs.configuration.frontend_compact = True
elif action == "reload_data": elif action == "reload_data":
Hacs().system.status.reloading_data = True hacs.system.status.reloading_data = True
hass.bus.async_fire("hacs/status", {}) hass.bus.async_fire("hacs/status", {})
await Hacs().recuring_tasks_all() await hacs.recuring_tasks_all()
Hacs().system.status.reloading_data = False hacs.system.status.reloading_data = False
hass.bus.async_fire("hacs/status", {}) hass.bus.async_fire("hacs/status", {})
elif action == "upgrade_all": elif action == "upgrade_all":
Hacs().system.status.upgrading_all = True hacs.system.status.upgrading_all = True
Hacs().system.status.background_task = True hacs.system.status.background_task = True
hass.bus.async_fire("hacs/status", {}) hass.bus.async_fire("hacs/status", {})
for repository in Hacs().repositories: for repository in hacs.repositories:
if repository.pending_upgrade: if repository.pending_upgrade:
repository.status.selected_tag = None repository.status.selected_tag = None
await repository.install() await repository.install()
Hacs().system.status.upgrading_all = False hacs.system.status.upgrading_all = False
Hacs().system.status.background_task = False hacs.system.status.background_task = False
hass.bus.async_fire("hacs/status", {}) hass.bus.async_fire("hacs/status", {})
hass.bus.async_fire("hacs/repository", {}) hass.bus.async_fire("hacs/repository", {})
elif action == "clear_new": elif action == "clear_new":
for repo in Hacs().repositories: for repo in hacs.repositories:
if msg.get("category") == repo.information.category: if msg.get("category") == repo.data.category:
if repo.status.new: if repo.status.new:
Hacs().logger.debug( hacs.logger.debug(
f"Clearing new flag from '{repo.information.full_name}'" f"Clearing new flag from '{repo.data.full_name}'"
) )
repo.status.new = False repo.status.new = False
else: else:
Hacs().logger.error(f"WS action '{action}' is not valid") hacs.logger.error(f"WS action '{action}' is not valid")
hass.bus.async_fire("hacs/config", {}) hass.bus.async_fire("hacs/config", {})
await Hacs().data.async_write() await hacs.data.async_write()
@websocket_api.async_response @websocket_api.async_response
@websocket_api.websocket_command({vol.Required("type"): "hacs/config"}) @websocket_api.websocket_command({vol.Required("type"): "hacs/config"})
async def hacs_config(hass, connection, msg): async def hacs_config(hass, connection, msg):
"""Handle get media player cover command.""" """Handle get media player cover command."""
config = Hacs().configuration hacs = get_hacs()
config = hacs.configuration
content = {} content = {}
content["frontend_mode"] = config.frontend_mode content["frontend_mode"] = config.frontend_mode
content["frontend_compact"] = config.frontend_compact content["frontend_compact"] = config.frontend_compact
content["onboarding_done"] = config.onboarding_done content["onboarding_done"] = config.onboarding_done
content["version"] = Hacs().version content["version"] = hacs.version
content["dev"] = config.dev content["dev"] = config.dev
content["debug"] = config.debug content["debug"] = config.debug
content["country"] = config.country content["country"] = config.country
content["experimental"] = config.experimental content["experimental"] = config.experimental
content["categories"] = Hacs().common.categories content["categories"] = hacs.common.categories
connection.send_message(websocket_api.result_message(msg["id"], content)) connection.send_message(websocket_api.result_message(msg["id"], content))
@@ -110,13 +114,14 @@ async def hacs_config(hass, connection, msg):
@websocket_api.websocket_command({vol.Required("type"): "hacs/status"}) @websocket_api.websocket_command({vol.Required("type"): "hacs/status"})
async def hacs_status(hass, connection, msg): async def hacs_status(hass, connection, msg):
"""Handle get media player cover command.""" """Handle get media player cover command."""
hacs = get_hacs()
content = { content = {
"startup": Hacs().system.status.startup, "startup": hacs.system.status.startup,
"background_task": Hacs().system.status.background_task, "background_task": hacs.system.status.background_task,
"lovelace_mode": Hacs().system.lovelace_mode, "lovelace_mode": hacs.system.lovelace_mode,
"reloading_data": Hacs().system.status.reloading_data, "reloading_data": hacs.system.status.reloading_data,
"upgrading_all": Hacs().system.status.upgrading_all, "upgrading_all": hacs.system.status.upgrading_all,
"disabled": Hacs().system.disabled, "disabled": hacs.system.disabled,
} }
connection.send_message(websocket_api.result_message(msg["id"], content)) connection.send_message(websocket_api.result_message(msg["id"], content))
@@ -125,30 +130,31 @@ async def hacs_status(hass, connection, msg):
@websocket_api.websocket_command({vol.Required("type"): "hacs/repositories"}) @websocket_api.websocket_command({vol.Required("type"): "hacs/repositories"})
async def hacs_repositories(hass, connection, msg): async def hacs_repositories(hass, connection, msg):
"""Handle get media player cover command.""" """Handle get media player cover command."""
repositories = Hacs().repositories hacs = get_hacs()
repositories = hacs.repositories
content = [] content = []
for repo in repositories: for repo in repositories:
if repo.information.category in Hacs().common.categories: if repo.data.category in hacs.common.categories:
data = { data = {
"additional_info": repo.information.additional_info, "additional_info": repo.information.additional_info,
"authors": repo.information.authors, "authors": repo.data.authors,
"available_version": repo.display_available_version, "available_version": repo.display_available_version,
"beta": repo.status.show_beta, "beta": repo.status.show_beta,
"can_install": repo.can_install, "can_install": repo.can_install,
"category": repo.information.category, "category": repo.data.category,
"country": repo.repository_manifest.country, "country": repo.data.country,
"config_flow": repo.config_flow, "config_flow": repo.config_flow,
"custom": repo.custom, "custom": repo.custom,
"default_branch": repo.information.default_branch, "default_branch": repo.data.default_branch,
"description": repo.information.description, "description": repo.data.description,
"domain": repo.manifest.get("domain"), "domain": repo.integration_manifest.get("domain"),
"downloads": repo.releases.last_release_object_downloads, "downloads": repo.releases.downloads,
"file_name": repo.information.file_name, "file_name": repo.data.file_name,
"first_install": repo.status.first_install, "first_install": repo.status.first_install,
"full_name": repo.information.full_name, "full_name": repo.data.full_name,
"hide": repo.status.hide, "hide": repo.status.hide,
"hide_default_branch": repo.repository_manifest.hide_default_branch, "hide_default_branch": repo.data.hide_default_branch,
"homeassistant": repo.repository_manifest.homeassistant, "homeassistant": repo.data.homeassistant,
"id": repo.information.uid, "id": repo.information.uid,
"info": repo.information.info, "info": repo.information.info,
"installed_version": repo.display_installed_version, "installed_version": repo.display_installed_version,
@@ -162,11 +168,11 @@ async def hacs_repositories(hass, connection, msg):
"pending_upgrade": repo.pending_upgrade, "pending_upgrade": repo.pending_upgrade,
"releases": repo.releases.published_tags, "releases": repo.releases.published_tags,
"selected_tag": repo.status.selected_tag, "selected_tag": repo.status.selected_tag,
"stars": repo.information.stars, "stars": repo.data.stargazers_count,
"state": repo.state, "state": repo.state,
"status_description": repo.display_status_description, "status_description": repo.display_status_description,
"status": repo.display_status, "status": repo.display_status,
"topics": repo.information.topics, "topics": repo.data.topics,
"updated_info": repo.status.updated_info, "updated_info": repo.status.updated_info,
"version_or_commit": repo.display_version_or_commit, "version_or_commit": repo.display_version_or_commit,
} }
@@ -186,6 +192,7 @@ async def hacs_repositories(hass, connection, msg):
) )
async def hacs_repository(hass, connection, msg): async def hacs_repository(hass, connection, msg):
"""Handle get media player cover command.""" """Handle get media player cover command."""
hacs = get_hacs()
try: try:
repo_id = msg.get("repository") repo_id = msg.get("repository")
action = msg.get("action") action = msg.get("action")
@@ -193,8 +200,8 @@ async def hacs_repository(hass, connection, msg):
if repo_id is None or action is None: if repo_id is None or action is None:
return return
repository = Hacs().get_by_id(repo_id) repository = hacs.get_by_id(repo_id)
Hacs().logger.debug(f"Running {action} for {repository.information.full_name}") hacs.logger.debug(f"Running {action} for {repository.data.full_name}")
if action == "update": if action == "update":
await repository.update_repository() await repository.update_repository()
@@ -230,17 +237,17 @@ async def hacs_repository(hass, connection, msg):
repository.remove() repository.remove()
elif action == "set_version": elif action == "set_version":
if msg["version"] == repository.information.default_branch: if msg["version"] == repository.data.default_branch:
repository.status.selected_tag = None repository.status.selected_tag = None
else: else:
repository.status.selected_tag = msg["version"] repository.status.selected_tag = msg["version"]
await repository.update_repository() await repository.update_repository()
else: else:
Hacs().logger.error(f"WS action '{action}' is not valid") hacs.logger.error(f"WS action '{action}' is not valid")
repository.state = None repository.state = None
await Hacs().data.async_write() await hacs.data.async_write()
except AIOGitHubException as exception: except AIOGitHubException as exception:
hass.bus.async_fire("hacs/error", {"message": str(exception)}) hass.bus.async_fire("hacs/error", {"message": str(exception)})
except AttributeError as exception: except AttributeError as exception:
@@ -262,6 +269,7 @@ async def hacs_repository(hass, connection, msg):
) )
async def hacs_repository_data(hass, connection, msg): async def hacs_repository_data(hass, connection, msg):
"""Handle get media player cover command.""" """Handle get media player cover command."""
hacs = get_hacs()
repo_id = msg.get("repository") repo_id = msg.get("repository")
action = msg.get("action") action = msg.get("action")
data = msg.get("data") data = msg.get("data")
@@ -273,12 +281,12 @@ async def hacs_repository_data(hass, connection, msg):
if "github." in repo_id: if "github." in repo_id:
repo_id = repo_id.split("github.com/")[1] repo_id = repo_id.split("github.com/")[1]
if repo_id in Hacs().common.skip: if repo_id in hacs.common.skip:
Hacs().common.skip.remove(repo_id) hacs.common.skip.remove(repo_id)
if not Hacs().get_by_name(repo_id): if not hacs.get_by_name(repo_id):
try: try:
registration = await Hacs().register_repository(repo_id, data.lower()) registration = await register_repository(repo_id, data.lower())
if registration is not None: if registration is not None:
raise HacsException(registration) raise HacsException(registration)
except Exception as exception: # pylint: disable=broad-except except Exception as exception: # pylint: disable=broad-except
@@ -299,15 +307,15 @@ async def hacs_repository_data(hass, connection, msg):
}, },
) )
repository = Hacs().get_by_name(repo_id) repository = hacs.get_by_name(repo_id)
else: else:
repository = Hacs().get_by_id(repo_id) repository = hacs.get_by_id(repo_id)
if repository is None: if repository is None:
hass.bus.async_fire("hacs/repository", {}) hass.bus.async_fire("hacs/repository", {})
return return
Hacs().logger.debug(f"Running {action} for {repository.information.full_name}") hacs.logger.debug(f"Running {action} for {repository.data.full_name}")
if action == "set_state": if action == "set_state":
repository.state = data repository.state = data
@@ -322,9 +330,9 @@ async def hacs_repository_data(hass, connection, msg):
else: else:
repository.state = None repository.state = None
Hacs().logger.error(f"WS action '{action}' is not valid") hacs.logger.error(f"WS action '{action}' is not valid")
await Hacs().data.async_write() await hacs.data.async_write()
@websocket_api.async_response @websocket_api.async_response

View File

@@ -11,13 +11,15 @@ cover:
covers: covers:
large_garage: large_garage:
device: !secret large_garage_id device: !secret large_garage_id
username: !secret garadget_username #username: !secret garadget_username
password: !secret garadget_password #password: !secret garadget_password
access_token: !secret garadget_access_token
name: large_garage name: large_garage
small_garage: small_garage:
device: !secret small_garage_id device: !secret small_garage_id
username: !secret garadget_username #username: !secret garadget_username
password: !secret garadget_password #password: !secret garadget_password
access_token: !secret garadget_access_token
name: small_garage name: small_garage
sensor: sensor:

View File

@@ -6,17 +6,11 @@
################################### ###################################
homeassistant: homeassistant:
customize_glob:
"sensor.skybell_*":
icon: mdi:camera-front
hidden: False
homebridge_hidden: True
group: group:
skybell: skybell:
name: Skybell HD Front Door name: Skybell HD Front Door
entities: entities:
- binary_sensor.skybell_front_door_button - binary_sensor.skybell_front_door_button
- binary_sensor.skybell_front_door_motion - binary_sensor.skybell_front_door_motion
@@ -63,7 +57,7 @@ switch:
## Doorbell Press ## Doorbell Press
automation: automation:
- alias: 'Log SkyBell Pressed Activity' - alias: 'Log SkyBell Pressed Activity'
trigger: trigger:
- platform: state - platform: state
entity_id: entity_id:
@@ -87,7 +81,7 @@ automation:
# Motion Sensing # Motion Sensing
- alias: 'Log SkyBell Motion detection' - alias: 'Log SkyBell Motion detection'
trigger: trigger:
- platform: event - platform: event
event_type: skybell_motion event_type: skybell_motion
@@ -98,7 +92,7 @@ automation:
# Turn SkyBell Light and Neato Schedule back on if it's turned off. Like any Good Watchdog. # Turn SkyBell Light and Neato Schedule back on if it's turned off. Like any Good Watchdog.
- alias: Automated Mismatch WatchDog! - alias: Automated Mismatch WatchDog!
trigger: trigger:
- platform: state - platform: state
entity_id: entity_id:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB