mirror of
https://github.com/CCOSTAN/Home-AssistantConfig.git
synced 2025-04-14 05:04:00 +00:00
Updated HACS and also fixed Garadget #727
This commit is contained in:
parent
51aab60dea
commit
e6e0d442e9
0
config/custom_components/hacs/.translations/da.json
Executable file → Normal file
0
config/custom_components/hacs/.translations/da.json
Executable file → Normal file
14
config/custom_components/hacs/.translations/de.json
Executable file → Normal file
14
config/custom_components/hacs/.translations/de.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integration",
|
||||
"integrations": "Integrationen",
|
||||
"manage": "verwalten",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Apps",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python Skript",
|
||||
@ -33,11 +35,12 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "AppDaemon App-Discovery & Tracking aktivieren",
|
||||
"python_script": "Python Script-Discovery & Tracking aktivieren",
|
||||
"appdaemon": "AppDaemon App-Entdeckung & Nachverfolgung aktivieren",
|
||||
"netdaemon": "NetDaemon App-Entdeckung & Nachverfolgung aktivieren",
|
||||
"python_script": "Python Script-Entdeckung & Nachverfolgung aktivieren",
|
||||
"sidepanel_icon": "Sidepanel Symbol",
|
||||
"sidepanel_title": "Sidepanel Titel",
|
||||
"theme": "Theme-Discovery & Tracking aktivieren",
|
||||
"theme": "Theme-Entdeckung & Nachverfolgung aktivieren",
|
||||
"token": "Persönlicher GitHub Zugriffstoken"
|
||||
},
|
||||
"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",
|
||||
"generic": "Bist du dir sicher?",
|
||||
"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_upgrades": "Keine Upgrades ausstehend",
|
||||
"ok": "OK",
|
||||
@ -70,10 +74,11 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "AppDaemon App-Discovery & Tracking aktivieren",
|
||||
"appdaemon": "AppDaemon App-Entdeckung & Nachverfolgung aktivieren",
|
||||
"country": "Nach Ländercode filtern.",
|
||||
"debug": "Debug aktivieren.",
|
||||
"experimental": "Experimentelle Funktionen aktivieren",
|
||||
"netdaemon": "NetDaemon App-Entdeckung & Nachverfolgung aktivieren",
|
||||
"not_in_use": "Nicht in Verwendung mit YAML",
|
||||
"release_limit": "Anzahl anzuzeigender Releases.",
|
||||
"sidepanel_icon": "Sidepanel Symbol",
|
||||
@ -115,6 +120,7 @@
|
||||
"note_installed": "Wird installiert nach",
|
||||
"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_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_plugin": "Plugin öffnen",
|
||||
"reinstall": "Neu installieren",
|
||||
|
7
config/custom_components/hacs/.translations/el.json
Executable file → Normal file
7
config/custom_components/hacs/.translations/el.json
Executable file → Normal file
@ -5,12 +5,15 @@
|
||||
"appdaemon_apps": "AppDaemon Apps",
|
||||
"background_task": "Τρέχει μια διεργασία στο παρασκήνιο, η σελίδα θα ανανεωθεί μόλις αυτό ολοκληρωθεί.",
|
||||
"check_log_file": "Ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες.",
|
||||
"continue": "Να συνεχίσει",
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
"documentation": "Τεκμηρίωση",
|
||||
"hacs_is_disabled": "Το HACS είναι απενεργοποιημένο",
|
||||
"installed": "εγκατεστημένο",
|
||||
"integration": "Ενσωμάτωση",
|
||||
"integrations": "Ενσωματωμένα",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Apps",
|
||||
"plugin": "Πρόσθετο",
|
||||
"plugins": "Πρόσθετα",
|
||||
"python_script": "Πρόγραμμα Python",
|
||||
@ -32,6 +35,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το AppDaemon",
|
||||
"netdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το NetDaemon",
|
||||
"python_script": "Ενεργοποίηση εύρεσης & παρακολούθησης για τα python_scripts",
|
||||
"sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ",
|
||||
"sidepanel_title": "Τίτλος πλαϊνού πάνελ",
|
||||
@ -50,6 +54,7 @@
|
||||
"cancel": "Ακύρωση",
|
||||
"continue": "Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
|
||||
"delete": "Είστε σίγουροι ότι θέλετε να διαγράψετε το '{item}';",
|
||||
"delete_installed": "Το '{item}' είναι εγκατεστημένο, πρέπει να το απεγκαταστήσετε πριν να το διαγράψετε.",
|
||||
"exist": "{item} υπάρχει ήδη",
|
||||
"generic": "Είστε βέβαιοι;",
|
||||
"home_assistant_is_restarting": "Περιμένετε, το Home Assistant επανεκκινείται τώρα.",
|
||||
@ -70,6 +75,7 @@
|
||||
"country": "Κριτήριο με βάση τον κωδικό χώρας.",
|
||||
"debug": "Ενεργοποίηση εντοπισμού σφαλμάτων.",
|
||||
"experimental": "Ενεργοποίση πειραματικών λειτουργιών",
|
||||
"netdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το NetDaemon",
|
||||
"release_limit": "Αριθμός εκδόσεων που να παραθέτονται.",
|
||||
"sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ",
|
||||
"sidepanel_title": "Τίτλος πλαϊνού πάνελ"
|
||||
@ -110,6 +116,7 @@
|
||||
"open_plugin": "Ανοιχτό πρόσθετο",
|
||||
"reinstall": "Επανεγκατάσταση",
|
||||
"repository": "Αποθετήριο",
|
||||
"restart_home_assistant": "Επανεκκίνηση του Home Assistant",
|
||||
"show_beta": "Εμφάνιση του beta",
|
||||
"uninstall": "Απεγκατάσταση",
|
||||
"update_information": "Ενημέρωση πληροφοριών",
|
||||
|
5
config/custom_components/hacs/.translations/en.json
Executable file → Normal file
5
config/custom_components/hacs/.translations/en.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integration",
|
||||
"integrations": "Integrations",
|
||||
"manage": "manage",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Apps",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python Script",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Enable AppDaemon apps discovery & tracking",
|
||||
"netdaemon": "Enable NetDaemon apps discovery & tracking",
|
||||
"python_script": "Enable python_scripts discovery & tracking",
|
||||
"sidepanel_icon": "Side panel icon",
|
||||
"sidepanel_title": "Side panel title",
|
||||
@ -75,6 +78,7 @@
|
||||
"country": "Filter with country code.",
|
||||
"debug": "Enable debug.",
|
||||
"experimental": "Enable experimental features",
|
||||
"netdaemon": "Enable NetDaemon apps discovery & tracking",
|
||||
"not_in_use": "Not in use with YAML",
|
||||
"release_limit": "Number of releases to show.",
|
||||
"sidepanel_icon": "Side panel icon",
|
||||
@ -116,6 +120,7 @@
|
||||
"note_installed": "When installed, this will be located in",
|
||||
"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_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_plugin": "Open plugin",
|
||||
"reinstall": "Reinstall",
|
||||
|
6
config/custom_components/hacs/.translations/es.json
Executable file → Normal file
6
config/custom_components/hacs/.translations/es.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integración",
|
||||
"integrations": "Integraciones",
|
||||
"manage": "Administrar",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Apps",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python Script",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Ícono del panel lateral",
|
||||
"sidepanel_title": "Título del panel lateral",
|
||||
@ -56,6 +59,7 @@
|
||||
"exist": "{item} ya existe",
|
||||
"generic": "¿Estás seguro?",
|
||||
"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_upgrades": "No hay actualizaciones pendientes",
|
||||
"ok": "OK",
|
||||
@ -74,6 +78,7 @@
|
||||
"country": "Filtrar por el código de país.",
|
||||
"debug": "Habilitar depuración.",
|
||||
"experimental": "Habilitar funciones experimentales",
|
||||
"netdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de NetDaemon",
|
||||
"not_in_use": "No usarse con YAML",
|
||||
"release_limit": "Número de versiones a mostrar.",
|
||||
"sidepanel_icon": "Icono del panel lateral",
|
||||
@ -84,6 +89,7 @@
|
||||
},
|
||||
"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_title": "Configuración de UI soportada",
|
||||
"integration_not_loaded": "Esta integración no se carga en Home Assistant.",
|
||||
"no_restart_required": "No es necesario reiniciar",
|
||||
"not_loaded": "No está cargado",
|
||||
|
4
config/custom_components/hacs/.translations/fr.json
Executable file → Normal file
4
config/custom_components/hacs/.translations/fr.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Intégration",
|
||||
"integrations": "Intégrations",
|
||||
"manage": "gérer",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "Applications NetDaemon",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Script Python",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Icône de la barre latérale",
|
||||
"sidepanel_title": "Titre de la barre latérale",
|
||||
@ -75,6 +78,7 @@
|
||||
"country": "Filtrer par code pays.",
|
||||
"debug": "Activez le débogage.",
|
||||
"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",
|
||||
"release_limit": "Nombre de recensés à afficher.",
|
||||
"sidepanel_icon": "Icône de la barre latérale",
|
||||
|
0
config/custom_components/hacs/.translations/hu.json
Executable file → Normal file
0
config/custom_components/hacs/.translations/hu.json
Executable file → Normal file
7
config/custom_components/hacs/.translations/it.json
Executable file → Normal file
7
config/custom_components/hacs/.translations/it.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integrazione",
|
||||
"integrations": "Integrazioni",
|
||||
"manage": "gestione",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "Applicazioni NetDaemon",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugin",
|
||||
"python_script": "Script python",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Icona nel pannello laterale",
|
||||
"sidepanel_title": "Titolo nel pannello laterale",
|
||||
@ -56,6 +59,7 @@
|
||||
"exist": "{item} esiste già",
|
||||
"generic": "Sei sicuro?",
|
||||
"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_upgrades": "Nessun aggiornamento in sospeso",
|
||||
"ok": "OK",
|
||||
@ -74,6 +78,8 @@
|
||||
"country": "Filtra con prefisso internazionale.",
|
||||
"debug": "Abilita debug.",
|
||||
"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.",
|
||||
"sidepanel_icon": "Icona nel pannello laterale",
|
||||
"sidepanel_title": "Titolo nel pannello laterale"
|
||||
@ -83,6 +89,7 @@
|
||||
},
|
||||
"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_title": "Configurazione dell'interfaccia utente supportata",
|
||||
"integration_not_loaded": "Questa integrazione non è caricata in Home Assistant.",
|
||||
"no_restart_required": "Non è necessario riavviare",
|
||||
"not_loaded": "Non caricato",
|
||||
|
4
config/custom_components/hacs/.translations/nb.json
Executable file → Normal file
4
config/custom_components/hacs/.translations/nb.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integrasjon",
|
||||
"integrations": "Integrasjoner",
|
||||
"manage": "manage",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Apper",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python-skript",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Sidepanel ikon",
|
||||
"sidepanel_title": "Sidepanel tittel",
|
||||
@ -75,6 +78,7 @@
|
||||
"country": "Filtrer med landskode.",
|
||||
"debug": "Aktiver debug",
|
||||
"experimental": "Aktiver eksperimentelle funksjoner",
|
||||
"netdaemon": "Aktiver oppdagelse og sporing av NetDaemon-apper",
|
||||
"not_in_use": "Ikke i bruk med YAML",
|
||||
"release_limit": "Antall utgivelser som skal vises.",
|
||||
"sidepanel_icon": "Sidepanel ikon",
|
||||
|
5
config/custom_components/hacs/.translations/nl.json
Executable file → Normal file
5
config/custom_components/hacs/.translations/nl.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integratie",
|
||||
"integrations": "Integraties",
|
||||
"manage": "beheer",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Apps",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python Script",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Zet AppDaemon apps ontdekken & traceren aan",
|
||||
"netdaemon": "Zet NetDaemon apps ontdekken & traceren aan",
|
||||
"python_script": "Zet python_scripts ontdekken & traceren aan",
|
||||
"sidepanel_icon": "Zijpaneel icoon",
|
||||
"sidepanel_title": "Zijpaneel titel",
|
||||
@ -74,6 +77,7 @@
|
||||
"country": "Filter met land code.",
|
||||
"debug": "Schakel debug in.",
|
||||
"experimental": "Zet experimentele functies aan",
|
||||
"netdaemon": "Zet NetDaemon apps ontdekken & traceren aan",
|
||||
"not_in_use": "Niet in gebruik met YAML",
|
||||
"release_limit": "Aantal releases om te laten zien.",
|
||||
"sidepanel_icon": "Zijpaneel icoon",
|
||||
@ -84,6 +88,7 @@
|
||||
},
|
||||
"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_title": "UI-configuratie ondersteund",
|
||||
"integration_not_loaded": "Deze integratie wordt niet geladen in Home Assistant.",
|
||||
"no_restart_required": "Geen herstart vereist",
|
||||
"not_loaded": "Niet geladen",
|
||||
|
86
config/custom_components/hacs/.translations/nn.json
Executable file → Normal file
86
config/custom_components/hacs/.translations/nn.json
Executable file → Normal file
@ -4,14 +4,17 @@
|
||||
"appdaemon": "AppDaemon",
|
||||
"appdaemon_apps": "AppDeamon-appar",
|
||||
"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",
|
||||
"documentation": "dokumentasjon",
|
||||
"documentation": "Dokumentasjon",
|
||||
"hacs_is_disabled": "HACS er deaktivert",
|
||||
"installed": "Installert",
|
||||
"integration": "Integrasjon",
|
||||
"integrations": "Integrasjonar",
|
||||
"manage": "manage",
|
||||
"manage": "Handtere",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDeamon-appar",
|
||||
"plugin": "Tillegg",
|
||||
"plugins": "Tillegg",
|
||||
"python_script": "Pythonskript",
|
||||
@ -33,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Aktiver AppDeamon-appar-oppdaging og sporing",
|
||||
"netdaemon": "Aktiver NetDeamon-appar-oppdaging og sporing",
|
||||
"python_script": "Aktiver pythonscript-oppdaging og sporing",
|
||||
"sidepanel_icon": "Sidepanelikon",
|
||||
"sidepanel_title": "Sidepaneltittel",
|
||||
@ -47,18 +51,23 @@
|
||||
},
|
||||
"confirm": {
|
||||
"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",
|
||||
"continue": "Er du sikker på at du vil halde fram?",
|
||||
"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?",
|
||||
"home_assistant_is_restarting": "Vent, Home Assistant starter nå 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_upgrades": "Ingen ventande oppgradringer",
|
||||
"ok": "OK",
|
||||
"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?",
|
||||
"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"
|
||||
},
|
||||
"options": {
|
||||
@ -69,6 +78,8 @@
|
||||
"country": "Filterer med landskode",
|
||||
"debug": "Aktiver debug.",
|
||||
"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",
|
||||
"sidepanel_icon": "Sidepanelikon",
|
||||
"sidepanel_title": "Sidepaneltittel"
|
||||
@ -77,8 +88,10 @@
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"no_restart_required": "Ingen omstart kreves",
|
||||
"no_restart_required": "Ingen omstart kravd",
|
||||
"not_loaded": "Ikkje lasta",
|
||||
"plugin_not_loaded": "Tillegget er ikkje lagt til i Lovelace-ressursane dine.",
|
||||
"restart": "Du må starte Home Assistant på nytt",
|
||||
@ -92,9 +105,9 @@
|
||||
"changelog": "Endre logg",
|
||||
"downloads": "Nedlastinger",
|
||||
"flag_this": "Marker dette",
|
||||
"frontend_version": "Frontend versjon",
|
||||
"frontend_version": "Frontend-versjon",
|
||||
"github_stars": "GitHub-stjerner",
|
||||
"goto_integrations": "Gå til integrasjoner",
|
||||
"goto_integrations": "Gå til integrasjonar",
|
||||
"hide": "Gøym",
|
||||
"hide_beta": "Gøym beta",
|
||||
"install": "Installer",
|
||||
@ -121,7 +134,7 @@
|
||||
"add_custom_repository": "LEGG TIL EIN ANNAN REPOSITORY",
|
||||
"adding_new_repo": "Legger til ny repository '{repo}'",
|
||||
"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",
|
||||
"compact_mode": "Kompaktmodus",
|
||||
"custom_repositories": "VANLEG REPOSITORY",
|
||||
@ -130,10 +143,11 @@
|
||||
"grid": "rutenett",
|
||||
"hacs_repo": "HACS repo",
|
||||
"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_window": "Last inn vinduet på nytt",
|
||||
"repository_configuration": "Repository konfigurasjon",
|
||||
"reload_window": "Last inn vindauget på nytt",
|
||||
"repository_configuration": "Repository-konfigurasjon",
|
||||
"save": "Lagre",
|
||||
"table": "Tavle",
|
||||
"table_view": "Tabellvisning",
|
||||
@ -141,43 +155,43 @@
|
||||
"upgrade_all": "Oppdater alle"
|
||||
},
|
||||
"store": {
|
||||
"ascending": "stigende",
|
||||
"ascending": "stigande",
|
||||
"clear_new": "Fjern alle nye repositories",
|
||||
"descending": "synkende",
|
||||
"descending": "synkande",
|
||||
"last_updated": "Sist oppdatert",
|
||||
"name": "Namn",
|
||||
"new_repositories": "Ny repository",
|
||||
"pending_upgrades": "Venter på oppgraderinger",
|
||||
"pending_upgrades": "Ventande oppgraderinger",
|
||||
"placeholder_search": "Ver vennleg og skriv inn ei søkefrase",
|
||||
"sort": "Sorter",
|
||||
"stars": "Stjerner",
|
||||
"status": "Status"
|
||||
},
|
||||
"time": {
|
||||
"ago": "siden",
|
||||
"ago": "sidan",
|
||||
"day": "dag",
|
||||
"days": "dager",
|
||||
"days": "dagar",
|
||||
"hour": "time",
|
||||
"hours": "timer",
|
||||
"hours": "timar",
|
||||
"minute": "minutt",
|
||||
"minutes": "minutter",
|
||||
"month": "måned",
|
||||
"months": "måneder",
|
||||
"one": "En",
|
||||
"one_day_ago": "for en dag siden",
|
||||
"one_hour_ago": "en time siden",
|
||||
"one_minute_ago": "ett minutt siden",
|
||||
"one_month_ago": "en måned siden",
|
||||
"one_second_ago": "ett sekund siden",
|
||||
"one_year_ago": "ett år siden",
|
||||
"minutes": "minutt",
|
||||
"month": "månad",
|
||||
"months": "månadar",
|
||||
"one": "Éin",
|
||||
"one_day_ago": "for éin dag sidan",
|
||||
"one_hour_ago": "éin time sidan",
|
||||
"one_minute_ago": "eitt minutt sidan",
|
||||
"one_month_ago": "ein månad sidan",
|
||||
"one_second_ago": "eitt sekund sidan",
|
||||
"one_year_ago": "eitt år sidan",
|
||||
"second": "sekund",
|
||||
"seconds": "sekunder",
|
||||
"x_days_ago": "{x} dager siden",
|
||||
"x_hours_ago": "{x} timer siden",
|
||||
"x_minutes_ago": "{x} minutter siden",
|
||||
"x_months_ago": "{x} måneder siden",
|
||||
"x_seconds_ago": "{x} sekunder siden",
|
||||
"x_years_ago": "{x} år siden",
|
||||
"seconds": "sekund",
|
||||
"x_days_ago": "{x} dagar siden",
|
||||
"x_hours_ago": "{x} timer sidan",
|
||||
"x_minutes_ago": "{x} minutt sidan",
|
||||
"x_months_ago": "{x} månadar sidan",
|
||||
"x_seconds_ago": "{x} sekund sidan",
|
||||
"x_years_ago": "{x} år sidan",
|
||||
"year": "år",
|
||||
"years": "år"
|
||||
}
|
||||
|
4
config/custom_components/hacs/.translations/pl.json
Executable file → Normal file
4
config/custom_components/hacs/.translations/pl.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integracja",
|
||||
"integrations": "Integracje",
|
||||
"manage": "zarządzaj",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "Aplikacje NetDaemon",
|
||||
"plugin": "Wtyczka",
|
||||
"plugins": "Wtyczki",
|
||||
"python_script": "Skrypt Python",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Ikona w panelu bocznym",
|
||||
"sidepanel_title": "Tytuł w panelu bocznym",
|
||||
@ -75,6 +78,7 @@
|
||||
"country": "Filtruj według kodu kraju",
|
||||
"debug": "Włącz debugowanie.",
|
||||
"experimental": "Włącz funkcje eksperymentalne",
|
||||
"netdaemon": "Włącz wykrywanie i śledzenie aplikacji NetDaemon",
|
||||
"not_in_use": "Nieużywany z YAML",
|
||||
"release_limit": "Liczba wydań do wyświetlenia",
|
||||
"sidepanel_icon": "Ikona w panelu bocznym",
|
||||
|
15
config/custom_components/hacs/.translations/pt-BR.json
Executable file → Normal file
15
config/custom_components/hacs/.translations/pt-BR.json
Executable file → Normal file
@ -12,6 +12,8 @@
|
||||
"integration": "Integração",
|
||||
"integrations": "Integrações",
|
||||
"manage": "gerenciar",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Apps",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python Script",
|
||||
@ -33,6 +35,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento",
|
||||
"netdaemon": "Habilitar NetDaemon apps descoberta & rastreamento",
|
||||
"python_script": "Habilitar python_scripts descoberta & rastreamento",
|
||||
"sidepanel_icon": "Icone do painel lateral",
|
||||
"sidepanel_title": "Titulo do painel lateral",
|
||||
@ -47,10 +50,13 @@
|
||||
},
|
||||
"confirm": {
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Tem certeza que quer continuar?",
|
||||
"delete": "Tem certeza de que deseja excluir '{item}'?",
|
||||
"exist": "{item} já existe",
|
||||
"home_assistant_is_restarting": "Espere, o Home Assistant está agora a reiniciar.",
|
||||
"no": "Não",
|
||||
"ok": "OK",
|
||||
"restart_home_assistant": "Tem certeza de que deseja reiniciar o Home Assistant?",
|
||||
"uninstall": "Tem certeza de que deseja desinstalar '{item}'?",
|
||||
"yes": "Sim"
|
||||
},
|
||||
@ -61,6 +67,8 @@
|
||||
"appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento",
|
||||
"country": "Filtro pelo código do país.",
|
||||
"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.",
|
||||
"sidepanel_icon": "Icone do painel lateral",
|
||||
"sidepanel_title": "Titulo do painel lateral"
|
||||
@ -69,6 +77,7 @@
|
||||
}
|
||||
},
|
||||
"repository_banner": {
|
||||
"integration_not_loaded": "Esta integração não é carregada no Home Assistant.",
|
||||
"no_restart_required": "Não é necessário reiniciar",
|
||||
"not_loaded": "Não carregado",
|
||||
"restart": "Você precisa reiniciar o Home Assistant.",
|
||||
@ -80,6 +89,7 @@
|
||||
"available": "Disponível",
|
||||
"back_to": "Voltar para",
|
||||
"changelog": "Changelog",
|
||||
"downloads": "Downloads",
|
||||
"flag_this": "Sinalizar isso",
|
||||
"frontend_version": "Versão Frontend",
|
||||
"github_stars": "Estrelas de GitHub",
|
||||
@ -118,12 +128,14 @@
|
||||
"grid": "Grade",
|
||||
"hacs_repo": "HACS repo",
|
||||
"hidden_repositories": "repositórios ocultos",
|
||||
"missing_category": "Você precisa selecionar uma categoria",
|
||||
"open_repository": "Repositório aberto",
|
||||
"reload_data": "Recarregar dados",
|
||||
"reload_window": "Recarregar janela",
|
||||
"repository_configuration": "Configuração do Repositório",
|
||||
"save": "Salvar",
|
||||
"table": "Tabela",
|
||||
"table_view": "Vista de mesa",
|
||||
"unhide": "reexibir",
|
||||
"upgrade_all": "Atualizar tudo"
|
||||
},
|
||||
@ -136,7 +148,8 @@
|
||||
"new_repositories": "Novos Repositórios",
|
||||
"pending_upgrades": "Atualizações pendentes",
|
||||
"placeholder_search": "Por favor insira um termo de pesquisa...",
|
||||
"stars": "Estrelas"
|
||||
"stars": "Estrelas",
|
||||
"status": "Status"
|
||||
},
|
||||
"time": {
|
||||
"ago": "atrás",
|
||||
|
17
config/custom_components/hacs/.translations/ro.json
Executable file → Normal file
17
config/custom_components/hacs/.translations/ro.json
Executable file → Normal file
@ -11,6 +11,8 @@
|
||||
"installed": "instalat",
|
||||
"integration": "Integrare",
|
||||
"integrations": "Integrări",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "Aplicații NetDaemon",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugin-uri",
|
||||
"python_script": "Script Python",
|
||||
@ -32,6 +34,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Pictogramă Panou lateral",
|
||||
"sidepanel_title": "Titlu panou lateral",
|
||||
@ -45,6 +48,7 @@
|
||||
"title": "HACS (Home Assistant Community Store)"
|
||||
},
|
||||
"confirm": {
|
||||
"bg_task": "Acțiunea este dezactivată în timp ce activitățile de fundal se execută.",
|
||||
"cancel": "Anulare",
|
||||
"delete": "Sigur doriți să ștergeți '{item}'?",
|
||||
"exist": "{item} există deja",
|
||||
@ -60,6 +64,7 @@
|
||||
"appdaemon": "Activați descoperirea și urmărirea aplicațiilor AppDaemon",
|
||||
"country": "Filtrează cu codul țării.",
|
||||
"experimental": "Activați funcțiile experimentale",
|
||||
"netdaemon": "Activați descoperirea și urmărirea aplicațiilor NetDaemon",
|
||||
"not_in_use": "Nu este utilizat cu YAML",
|
||||
"release_limit": "Număr de versiuni afișate.",
|
||||
"sidepanel_icon": "Pictogramă Panou lateral",
|
||||
@ -69,8 +74,10 @@
|
||||
}
|
||||
},
|
||||
"repository_banner": {
|
||||
"integration_not_loaded": "Această integrare nu este încărcată în Home Assistant.",
|
||||
"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": {
|
||||
"add_to_lovelace": "Adăugați la Lovelace",
|
||||
@ -81,6 +88,7 @@
|
||||
"downloads": "Descărcări",
|
||||
"flag_this": "Semnalizează",
|
||||
"frontend_version": "Versiune frontend",
|
||||
"github_stars": "Stele GitHub",
|
||||
"hide": "Ascunde",
|
||||
"hide_beta": "Ascundere beta",
|
||||
"install": "Instalează",
|
||||
@ -97,6 +105,7 @@
|
||||
"open_plugin": "Deschide plugin",
|
||||
"reinstall": "Reinstalare",
|
||||
"repository": "Depozit",
|
||||
"restart_home_assistant": "Reporniți Home Assistant",
|
||||
"show_beta": "Afișare beta",
|
||||
"uninstall": "Dezinstalare",
|
||||
"update_information": "Actualizare informație",
|
||||
@ -114,8 +123,11 @@
|
||||
"grid": "Grilă",
|
||||
"hacs_repo": "HACS repo",
|
||||
"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_window": "Reîncărcați fereastra",
|
||||
"repository_configuration": "Configurația depozitului",
|
||||
"save": "Salveaza",
|
||||
"table": "Tabel",
|
||||
"table_view": "Vizualizare tabel",
|
||||
@ -130,7 +142,8 @@
|
||||
"new_repositories": "Noi depozite",
|
||||
"placeholder_search": "Vă rugăm să introduceți un termen de căutare ...",
|
||||
"sort": "fel",
|
||||
"stars": "Stele"
|
||||
"stars": "Stele",
|
||||
"status": "Starea"
|
||||
},
|
||||
"time": {
|
||||
"ago": "în urmă",
|
||||
|
8
config/custom_components/hacs/.translations/ru.json
Executable file → Normal file
8
config/custom_components/hacs/.translations/ru.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Интеграция",
|
||||
"integrations": "Интеграции",
|
||||
"manage": "управлять",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "Приложения NetDaemon",
|
||||
"plugin": "Плагин",
|
||||
"plugins": "Плагины",
|
||||
"python_script": "Скрипт Python",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Включить поиск и отслеживание приложений AppDaemon",
|
||||
"netdaemon": "Включить поиск и отслеживание приложений NetDaemon",
|
||||
"python_script": "Включить поиск и отслеживание скриптов Python",
|
||||
"sidepanel_icon": "Иконка в боковом меню",
|
||||
"sidepanel_title": "Название в боковом меню",
|
||||
@ -69,9 +72,10 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Включить поиск и отслеживание приложений AppDaemon",
|
||||
"country": "Фильтр с кодом страны.",
|
||||
"country": "Фильтр по стране.",
|
||||
"debug": "Включить отладку.",
|
||||
"experimental": "Включить экспериментальные функции",
|
||||
"experimental": "Вкл. экспериментальные функции",
|
||||
"netdaemon": "Включить поиск и отслеживание приложений NetDaemon",
|
||||
"not_in_use": "Не используется с YAML",
|
||||
"release_limit": "Число доступных версий.",
|
||||
"sidepanel_icon": "Иконка в боковом меню",
|
||||
|
6
config/custom_components/hacs/.translations/sl.json
Executable file → Normal file
6
config/custom_components/hacs/.translations/sl.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "Integracija",
|
||||
"integrations": "Integracije",
|
||||
"manage": "upravljanje",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Aplikacije",
|
||||
"plugin": "vtičnik",
|
||||
"plugins": "Vtičniki",
|
||||
"python_script": "Python skripta",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Ikona stranske plošče",
|
||||
"sidepanel_title": "Naslov stranske plošče",
|
||||
@ -56,6 +59,7 @@
|
||||
"exist": "{item} že obstaja",
|
||||
"generic": "Ali si prepričan?",
|
||||
"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_upgrades": "Ni nadgradenj",
|
||||
"ok": "V redu",
|
||||
@ -74,6 +78,7 @@
|
||||
"country": "Filtrirajte s kodo države.",
|
||||
"debug": "Omogoči odpravljanje napak.",
|
||||
"experimental": "Omogočite poskusne funkcije",
|
||||
"netdaemon": "Omogoči odkrivanje in sledenje aplikacij NetDaemon",
|
||||
"not_in_use": "Ni v uporabi z YAML",
|
||||
"release_limit": "Število izdaj, ki jih želite prikazati.",
|
||||
"sidepanel_icon": "Ikona stranske plošče",
|
||||
@ -84,6 +89,7 @@
|
||||
},
|
||||
"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_title": "Konfiguracija uporabniškega vmesnika je podprta",
|
||||
"integration_not_loaded": "Ta integracija ni naložena v programu Home Assistant.",
|
||||
"no_restart_required": "Ponovni zagon ni potreben",
|
||||
"not_loaded": "Ni naloženo",
|
||||
|
7
config/custom_components/hacs/.translations/sv.json
Executable file → Normal file
7
config/custom_components/hacs/.translations/sv.json
Executable file → Normal file
@ -11,6 +11,8 @@
|
||||
"installed": "installerad",
|
||||
"integration": "Integration",
|
||||
"integrations": "Integrationer",
|
||||
"netdaemon": "NetDaemon",
|
||||
"netdaemon_apps": "NetDaemon Applikationer",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python skript",
|
||||
@ -32,6 +34,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"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",
|
||||
"sidepanel_icon": "Ikon för sidpanel",
|
||||
"sidepanel_title": "Rubrik för sidpanel",
|
||||
@ -70,6 +73,7 @@
|
||||
"country": "Filtrera på landskod.",
|
||||
"debug": "Aktivera felsökning",
|
||||
"experimental": "Använd experimentella funktioner",
|
||||
"netdaemon": "Upptäck och följ NetDaemon applikationer",
|
||||
"release_limit": "Antalet releaser som visas.",
|
||||
"sidepanel_icon": "Ikon för sidpanel",
|
||||
"sidepanel_title": "Rubrik för sidpanel"
|
||||
@ -112,6 +116,7 @@
|
||||
"open_plugin": "Öppna plugin",
|
||||
"reinstall": "Ominstallera",
|
||||
"repository": "Repository",
|
||||
"restart_home_assistant": "Starta om Home Assistant",
|
||||
"show_beta": "Visa betaversioner",
|
||||
"uninstall": "Avinstallera",
|
||||
"update_information": "Uppdatera information",
|
||||
@ -131,6 +136,7 @@
|
||||
"hacs_repo": "HACS repo",
|
||||
"hidden_repositories": "dolda förråd",
|
||||
"missing_category": "Du behöver välja en kategori",
|
||||
"open_repository": "Öppna Repository",
|
||||
"reload_data": "Ladda om data",
|
||||
"reload_window": "Ladda om fönstret",
|
||||
"save": "Spara",
|
||||
@ -167,6 +173,7 @@
|
||||
"one_minute_ago": "en minut sedan",
|
||||
"one_month_ago": "en månad sedan",
|
||||
"one_second_ago": "för en sekund sedan",
|
||||
"one_year_ago": "ett år sedan",
|
||||
"second": "andra",
|
||||
"seconds": "sekunder",
|
||||
"x_days_ago": "{x} dagar sedan",
|
||||
|
14
config/custom_components/hacs/.translations/zh-Hans.json
Executable file → Normal file
14
config/custom_components/hacs/.translations/zh-Hans.json
Executable file → Normal file
@ -13,6 +13,8 @@
|
||||
"integration": "自定义组件",
|
||||
"integrations": "自定义组件",
|
||||
"manage": "管理",
|
||||
"netdaemon": "NetDaemon应用",
|
||||
"netdaemon_apps": "NetDaemon应用",
|
||||
"plugin": "Lovelace插件",
|
||||
"plugins": "Lovelace插件",
|
||||
"python_script": "Python脚本",
|
||||
@ -34,6 +36,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "启用AppDaemon应用发现和跟踪",
|
||||
"netdaemon": "启用NetDaemon应用发现和跟踪",
|
||||
"python_script": "启用python_scripts发现和跟踪",
|
||||
"sidepanel_icon": "侧面板图标",
|
||||
"sidepanel_title": "侧面板标题",
|
||||
@ -52,12 +55,12 @@
|
||||
"cancel": "取消",
|
||||
"continue": "你确定你要继续吗?",
|
||||
"delete": "是否确实要删除\"{item}\"?",
|
||||
"delete_installed": "已安装“ {item}”,需要先将其卸载,然后才能将其删除。",
|
||||
"delete_installed": "已安装 '{item}',需要先将其卸载,然后才能将其删除。",
|
||||
"exist": "{item}已经存在",
|
||||
"generic": "你确定吗?",
|
||||
"home_assistant_is_restarting": "请等待,Home Assistant现在正在重新启动。",
|
||||
"home_assistant_version_not_correct": "您正在运行Home Assistant的版本为'{haversion}',但是这个需要安装最低版本是'{minversion}'。",
|
||||
"no": "不",
|
||||
"no": "取消",
|
||||
"no_upgrades": "暂无升级",
|
||||
"ok": "确定",
|
||||
"overwrite": "这样做会覆盖它。",
|
||||
@ -65,7 +68,7 @@
|
||||
"restart_home_assistant": "您确定要重新启动Home Assistant吗?",
|
||||
"uninstall": "您确定要卸载“ {item} ”吗?",
|
||||
"upgrade_all": "这将升级所有这些仓库,请确保在继续之前已阅读所有仓库的发行说明。",
|
||||
"yes": "是"
|
||||
"yes": "确定"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
@ -75,6 +78,7 @@
|
||||
"country": "用国家代码过滤。",
|
||||
"debug": "启用调试。",
|
||||
"experimental": "启用实验功能",
|
||||
"netdaemon": "启用NetDaemon应用发现和跟踪",
|
||||
"not_in_use": "不适用于 YAML",
|
||||
"release_limit": "要显示的发行数量。",
|
||||
"sidepanel_icon": "侧面板图标",
|
||||
@ -128,8 +132,8 @@
|
||||
},
|
||||
"settings": {
|
||||
"add_custom_repository": "添加自定义仓库",
|
||||
"adding_new_repo": "添加新的仓库“ {repo}”",
|
||||
"adding_new_repo_category": "类别为“ {category}”。",
|
||||
"adding_new_repo": "添加新的仓库 '{repo}'",
|
||||
"adding_new_repo_category": "类别为 '{category}'。",
|
||||
"bg_task_custom": "自定义仓库在后台任务运行时被隐藏。",
|
||||
"category": "类别",
|
||||
"compact_mode": "紧凑模式",
|
||||
|
105
config/custom_components/hacs/__init__.py
Executable file → Normal file
105
config/custom_components/hacs/__init__.py
Executable file → Normal file
@ -4,6 +4,7 @@ Custom element manager for community created elements.
|
||||
For more details about this integration, please refer to the documentation at
|
||||
https://hacs.xyz/
|
||||
"""
|
||||
|
||||
import voluptuous as vol
|
||||
from aiogithubapi import AIOGitHub
|
||||
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.event import async_call_later
|
||||
|
||||
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
|
||||
from .const import DOMAIN, ELEMENT_TYPES, STARTUP, VERSION
|
||||
from .constrains import check_constans
|
||||
from .hacsbase import Hacs
|
||||
from .hacsbase.configuration import Configuration
|
||||
from .hacsbase.data import HacsData
|
||||
from .setup import add_sensor, load_hacs_repository, setup_frontend
|
||||
from custom_components.hacs.configuration_schema import (
|
||||
hacs_base_config_schema,
|
||||
hacs_config_option_schema,
|
||||
)
|
||||
from custom_components.hacs.const import DOMAIN, ELEMENT_TYPES, STARTUP, VERSION
|
||||
from custom_components.hacs.constrains import check_constans, check_requirements
|
||||
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[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):
|
||||
"""Set up this integration using yaml."""
|
||||
hacs = get_hacs()
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
hass.data[DOMAIN] = config
|
||||
Hacs.hass = hass
|
||||
Hacs.configuration = Configuration.from_dict(
|
||||
hacs.hass = hass
|
||||
hacs.session = async_create_clientsession(hass)
|
||||
hacs.configuration = Configuration.from_dict(
|
||||
config[DOMAIN], config[DOMAIN].get("options")
|
||||
)
|
||||
Hacs.configuration.config = config
|
||||
Hacs.configuration.config_type = "yaml"
|
||||
await startup_wrapper_for_yaml(Hacs)
|
||||
hacs.configuration.config = config
|
||||
hacs.configuration.config_type = "yaml"
|
||||
await startup_wrapper_for_yaml()
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
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):
|
||||
"""Set up this integration using UI."""
|
||||
hacs = get_hacs()
|
||||
conf = hass.data.get(DOMAIN)
|
||||
if config_entry.source == config_entries.SOURCE_IMPORT:
|
||||
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)
|
||||
)
|
||||
return False
|
||||
Hacs.hass = hass
|
||||
|
||||
Hacs.configuration = Configuration.from_dict(
|
||||
hacs.hass = hass
|
||||
hacs.session = async_create_clientsession(hass)
|
||||
hacs.configuration = Configuration.from_dict(
|
||||
config_entry.data, config_entry.options
|
||||
)
|
||||
Hacs.configuration.config_type = "flow"
|
||||
Hacs.configuration.config_entry = config_entry
|
||||
hacs.configuration.config_type = "flow"
|
||||
hacs.configuration.config_entry = config_entry
|
||||
config_entry.add_update_listener(reload_hacs)
|
||||
startup_result = await hacs_startup(Hacs)
|
||||
startup_result = await hacs_startup()
|
||||
if not startup_result:
|
||||
Hacs.system.disabled = True
|
||||
hacs.system.disabled = True
|
||||
raise ConfigEntryNotReady
|
||||
Hacs.system.disabled = False
|
||||
hacs.system.disabled = False
|
||||
return startup_result
|
||||
|
||||
|
||||
async def startup_wrapper_for_yaml(hacs):
|
||||
async def startup_wrapper_for_yaml():
|
||||
"""Startup wrapper for yaml config."""
|
||||
startup_result = await hacs_startup(hacs)
|
||||
hacs = get_hacs()
|
||||
startup_result = await hacs_startup()
|
||||
if not startup_result:
|
||||
hacs.system.disabled = True
|
||||
hacs.hass.components.frontend.async_remove_panel(
|
||||
@ -83,13 +98,16 @@ async def startup_wrapper_for_yaml(hacs):
|
||||
.replace("-", "_")
|
||||
)
|
||||
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
|
||||
hacs.system.disabled = False
|
||||
|
||||
|
||||
async def hacs_startup(hacs):
|
||||
async def hacs_startup():
|
||||
"""HACS startup tasks."""
|
||||
hacs = get_hacs()
|
||||
if not check_requirements():
|
||||
return False
|
||||
if hacs.configuration.debug:
|
||||
try:
|
||||
await hacs.hass.services.async_call(
|
||||
@ -115,20 +133,21 @@ async def hacs_startup(hacs):
|
||||
hacs.data = HacsData()
|
||||
|
||||
# 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_entry is not None:
|
||||
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
|
||||
return False
|
||||
|
||||
# Set up frontend
|
||||
await setup_frontend(hacs)
|
||||
await setup_frontend()
|
||||
|
||||
# Set up sensor
|
||||
await hacs.hass.async_add_executor_job(add_sensor, hacs)
|
||||
if not await hacs.hass.async_add_executor_job(internet_connectivity_check):
|
||||
hacs.logger.critical("No network connectivity")
|
||||
return False
|
||||
|
||||
# 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_entry is not None:
|
||||
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
|
||||
@ -136,7 +155,7 @@ async def hacs_startup(hacs):
|
||||
|
||||
# Restore from storefiles
|
||||
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
|
||||
if hacs.configuration.config_type == "flow":
|
||||
if hacs.configuration.config_entry is not None:
|
||||
@ -147,6 +166,8 @@ async def hacs_startup(hacs):
|
||||
hacs.common.categories = ELEMENT_TYPES
|
||||
if hacs.configuration.appdaemon:
|
||||
hacs.common.categories.append("appdaemon")
|
||||
if hacs.configuration.netdaemon:
|
||||
hacs.common.categories.append("netdaemon")
|
||||
if hacs.configuration.python_script:
|
||||
hacs.configuration.python_script = False
|
||||
if hacs.configuration.config_type == "yaml":
|
||||
@ -162,37 +183,39 @@ async def hacs_startup(hacs):
|
||||
|
||||
# Setup startup tasks
|
||||
if hacs.configuration.config_type == "yaml":
|
||||
hacs.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, hacs().startup_tasks()
|
||||
)
|
||||
hacs.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, hacs.startup_tasks())
|
||||
else:
|
||||
async_call_later(hacs.hass, 5, hacs().startup_tasks())
|
||||
async_call_later(hacs.hass, 5, hacs.startup_tasks())
|
||||
|
||||
# Show the configuration
|
||||
hacs.configuration.print()
|
||||
|
||||
# Set up sensor
|
||||
await hacs.hass.async_add_executor_job(add_sensor)
|
||||
|
||||
# Mischief managed!
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass, config_entry):
|
||||
"""Handle removal of an entry."""
|
||||
Hacs().logger.info("Disabling HACS")
|
||||
Hacs().logger.info("Removing recuring tasks")
|
||||
for task in Hacs().recuring_tasks:
|
||||
hacs = get_hacs()
|
||||
hacs.logger.info("Disabling HACS")
|
||||
hacs.logger.info("Removing recuring tasks")
|
||||
for task in hacs.recuring_tasks:
|
||||
task()
|
||||
Hacs().logger.info("Removing sensor")
|
||||
hacs.logger.info("Removing sensor")
|
||||
try:
|
||||
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
|
||||
except ValueError:
|
||||
pass
|
||||
Hacs().logger.info("Removing sidepanel")
|
||||
hacs.logger.info("Removing sidepanel")
|
||||
try:
|
||||
hass.components.frontend.async_remove_panel("hacs")
|
||||
except AttributeError:
|
||||
pass
|
||||
Hacs().system.disabled = True
|
||||
Hacs().logger.info("HACS is now disabled")
|
||||
hacs.system.disabled = True
|
||||
hacs.logger.info("HACS is now disabled")
|
||||
|
||||
|
||||
async def reload_hacs(hass, config_entry):
|
||||
|
13
config/custom_components/hacs/config_flow.py
Executable file → Normal file
13
config/custom_components/hacs/config_flow.py
Executable file → Normal file
@ -2,15 +2,16 @@
|
||||
# pylint: disable=dangerous-default-value
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from aiogithubapi import AIOGitHub, AIOGitHubException, AIOGitHubAuthentication
|
||||
from aiogithubapi import AIOGitHubException, AIOGitHubAuthentication
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import Hacs
|
||||
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__)
|
||||
|
||||
@ -46,7 +47,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Show the configuration form to edit location data."""
|
||||
return self.async_show_form(
|
||||
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,
|
||||
)
|
||||
|
||||
@ -69,8 +70,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Return true if token is valid."""
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
client = AIOGitHub(token, session)
|
||||
await client.get_repo("hacs/org")
|
||||
await get_repository(session, token, "hacs/org")
|
||||
return True
|
||||
except (AIOGitHubException, AIOGitHubAuthentication) as exception:
|
||||
_LOGGER.error(exception)
|
||||
@ -90,10 +90,11 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
hacs = get_hacs()
|
||||
if user_input is not None:
|
||||
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}
|
||||
else:
|
||||
schema = hacs_config_option_schema(self.config_entry.options)
|
||||
|
13
config/custom_components/hacs/configuration_schema.py
Executable file → Normal file
13
config/custom_components/hacs/configuration_schema.py
Executable file → Normal file
@ -8,6 +8,7 @@ TOKEN = "token"
|
||||
SIDEPANEL_TITLE = "sidepanel_title"
|
||||
SIDEPANEL_ICON = "sidepanel_icon"
|
||||
APPDAEMON = "appdaemon"
|
||||
NETDAEMON = "netdaemon"
|
||||
PYTHON_SCRIPT = "python_script"
|
||||
THEME = "theme"
|
||||
|
||||
@ -18,7 +19,7 @@ RELEASE_LIMIT = "release_limit"
|
||||
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."""
|
||||
if not config:
|
||||
config = {
|
||||
@ -26,14 +27,24 @@ def hacs_base_config_schema(config: dict = {}) -> dict:
|
||||
SIDEPANEL_ICON: "mdi:alpha-c-box",
|
||||
SIDEPANEL_TITLE: "HACS",
|
||||
APPDAEMON: False,
|
||||
NETDAEMON: False,
|
||||
PYTHON_SCRIPT: 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 {
|
||||
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,
|
||||
vol.Optional(PYTHON_SCRIPT, default=config.get(PYTHON_SCRIPT)): bool,
|
||||
vol.Optional(THEME, default=config.get(THEME)): bool,
|
||||
}
|
||||
|
2
config/custom_components/hacs/const.py
Executable file → Normal file
2
config/custom_components/hacs/const.py
Executable file → Normal file
@ -1,7 +1,7 @@
|
||||
"""Constants for HACS"""
|
||||
NAME_LONG = "HACS (Home Assistant Community Store)"
|
||||
NAME_SHORT = "HACS"
|
||||
VERSION = "0.21.5"
|
||||
VERSION = "0.23.2"
|
||||
DOMAIN = "hacs"
|
||||
PROJECT_URL = "https://github.com/hacs/integration/"
|
||||
CUSTOM_UPDATER_LOCATIONS = [
|
||||
|
62
config/custom_components/hacs/constrains.py
Executable file → Normal file
62
config/custom_components/hacs/constrains.py
Executable file → Normal file
@ -5,22 +5,25 @@ import os
|
||||
from .const import CUSTOM_UPDATER_LOCATIONS, CUSTOM_UPDATER_WARNING
|
||||
from .helpers.misc import version_left_higher_then_right
|
||||
|
||||
from custom_components.hacs.globals import get_hacs
|
||||
|
||||
MINIMUM_HA_VERSION = "0.98.0"
|
||||
|
||||
|
||||
def check_constans(hacs):
|
||||
def check_constans():
|
||||
"""Check HACS constrains."""
|
||||
if not constrain_translations(hacs):
|
||||
if not constrain_translations():
|
||||
return False
|
||||
if not constrain_custom_updater(hacs):
|
||||
if not constrain_custom_updater():
|
||||
return False
|
||||
if not constrain_version(hacs):
|
||||
if not constrain_version():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def constrain_custom_updater(hacs):
|
||||
def constrain_custom_updater():
|
||||
"""Check if custom_updater exist."""
|
||||
hacs = get_hacs()
|
||||
for location in CUSTOM_UPDATER_LOCATIONS:
|
||||
if os.path.exists(location.format(hacs.system.config_path)):
|
||||
msg = CUSTOM_UPDATER_WARNING.format(
|
||||
@ -31,8 +34,9 @@ def constrain_custom_updater(hacs):
|
||||
return True
|
||||
|
||||
|
||||
def constrain_version(hacs):
|
||||
def constrain_version():
|
||||
"""Check if the version is valid."""
|
||||
hacs = get_hacs()
|
||||
if not version_left_higher_then_right(hacs.system.ha_version, MINIMUM_HA_VERSION):
|
||||
hacs.logger.critical(
|
||||
f"You need HA version {MINIMUM_HA_VERSION} or newer to use this integration."
|
||||
@ -41,11 +45,55 @@ def constrain_version(hacs):
|
||||
return True
|
||||
|
||||
|
||||
def constrain_translations(hacs):
|
||||
def constrain_translations():
|
||||
"""Check if traslations exist."""
|
||||
hacs = get_hacs()
|
||||
if not os.path.exists(
|
||||
f"{hacs.system.config_path}/custom_components/hacs/.translations"
|
||||
):
|
||||
hacs.logger.critical("You are missing the translations directory.")
|
||||
return False
|
||||
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
|
||||
|
29
config/custom_components/hacs/globals.py
Normal file
29
config/custom_components/hacs/globals.py
Normal 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
127
config/custom_components/hacs/hacsbase/__init__.py
Executable file → Normal file
@ -9,13 +9,20 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
|
||||
from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit
|
||||
from integrationhelper import Logger
|
||||
|
||||
from .task_factory import HacsTaskFactory
|
||||
from .exceptions import HacsException
|
||||
from custom_components.hacs.hacsbase.task_factory import HacsTaskFactory
|
||||
from custom_components.hacs.hacsbase.exceptions import HacsException
|
||||
|
||||
from ..const import ELEMENT_TYPES
|
||||
from ..setup import setup_extra_stores
|
||||
from ..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.const import ELEMENT_TYPES
|
||||
from custom_components.hacs.setup import setup_extra_stores
|
||||
from custom_components.hacs.store import async_load_from_store, async_save_to_store
|
||||
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:
|
||||
@ -40,7 +47,6 @@ class HacsCommon:
|
||||
"""Common for HACS."""
|
||||
|
||||
categories = []
|
||||
blacklist = []
|
||||
default = []
|
||||
installed = []
|
||||
skip = []
|
||||
@ -90,6 +96,7 @@ class Hacs:
|
||||
github = None
|
||||
hass = None
|
||||
version = None
|
||||
session = None
|
||||
factory = HacsTaskFactory()
|
||||
system = System()
|
||||
recuring_tasks = []
|
||||
@ -114,7 +121,7 @@ class Hacs:
|
||||
"""Get repository by full_name."""
|
||||
try:
|
||||
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
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
@ -122,10 +129,9 @@ class Hacs:
|
||||
|
||||
def is_known(self, repository_full_name):
|
||||
"""Return a bool if the repository is known."""
|
||||
for repository in self.repositories:
|
||||
if repository.information.full_name == repository_full_name:
|
||||
return True
|
||||
return False
|
||||
return repository_full_name.lower() in [
|
||||
x.data.full_name.lower() for x in self.repositories
|
||||
]
|
||||
|
||||
@property
|
||||
def sorted_by_name(self):
|
||||
@ -135,59 +141,16 @@ class Hacs:
|
||||
@property
|
||||
def sorted_by_repository_name(self):
|
||||
"""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):
|
||||
"""Register a repository."""
|
||||
from ..repositories.repository import RERPOSITORY_CLASSES
|
||||
|
||||
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)
|
||||
await register_repository(full_name, category, check=True)
|
||||
|
||||
async def startup_tasks(self):
|
||||
"""Tasks tha are started after startup."""
|
||||
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.logger.debug(self.github.ratelimits.remaining)
|
||||
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()
|
||||
await self.load_known_repositories()
|
||||
await self.clear_out_blacklisted_repositories()
|
||||
await self.clear_out_removed_repositories()
|
||||
|
||||
self.recuring_tasks.append(
|
||||
async_track_time_interval(
|
||||
@ -257,7 +220,8 @@ class Hacs:
|
||||
stored_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"])
|
||||
|
||||
stored = {
|
||||
@ -266,7 +230,6 @@ class Hacs:
|
||||
"link": repository["link"],
|
||||
"acknowledged": True,
|
||||
}
|
||||
|
||||
if repository["repository"] not in instored:
|
||||
if repo is not None and repo.installed:
|
||||
self.logger.critical(
|
||||
@ -278,6 +241,7 @@ class Hacs:
|
||||
repo.remove()
|
||||
await repo.uninstall()
|
||||
stored_critical.append(stored)
|
||||
removed_repo.update_data(stored)
|
||||
|
||||
# Save to FS
|
||||
await async_save_to_store(self.hass, "critical", stored_critical)
|
||||
@ -299,7 +263,7 @@ class Hacs:
|
||||
for repository in self.repositories:
|
||||
if (
|
||||
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))
|
||||
|
||||
@ -313,33 +277,35 @@ class Hacs:
|
||||
async def recuring_tasks_all(self, notarealarg=None):
|
||||
"""Recuring tasks 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.hass.bus.async_fire("hacs/status", {})
|
||||
self.logger.debug(self.github.ratelimits.remaining)
|
||||
self.logger.debug(self.github.ratelimits.reset_utc)
|
||||
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))
|
||||
|
||||
await self.factory.execute()
|
||||
await self.load_known_repositories()
|
||||
await self.clear_out_blacklisted_repositories()
|
||||
await self.clear_out_removed_repositories()
|
||||
self.system.status.background_task = False
|
||||
await self.data.async_write()
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
|
||||
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."""
|
||||
need_to_save = False
|
||||
for repository in self.common.blacklist:
|
||||
if self.is_known(repository):
|
||||
repository = self.get_by_name(repository)
|
||||
if repository.status.installed:
|
||||
for removed in removed_repositories:
|
||||
if self.is_known(removed.repository):
|
||||
repository = self.get_by_name(removed.repository)
|
||||
if repository.status.installed and removed.removal_type != "critical":
|
||||
self.logger.warning(
|
||||
f"You have {repository.information.full_name} installed with HACS "
|
||||
+ "this repository has been blacklisted, please consider removing it."
|
||||
f"You have {repository.data.full_name} installed with HACS "
|
||||
+ f"this repository has been removed, please consider removing it. "
|
||||
+ f"Removal reason ({removed.removal_type})"
|
||||
)
|
||||
else:
|
||||
need_to_save = True
|
||||
@ -353,7 +319,7 @@ class Hacs:
|
||||
repositories = {}
|
||||
for category in self.common.categories:
|
||||
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)
|
||||
for repo in org:
|
||||
@ -370,17 +336,20 @@ class Hacs:
|
||||
self.logger.info("Loading known repositories")
|
||||
repositories = await self.get_repositories()
|
||||
|
||||
for item in await get_default_repos_lists(self.github, "blacklist"):
|
||||
if item not in self.common.blacklist:
|
||||
self.common.blacklist.append(item)
|
||||
for item in await get_default_repos_lists(
|
||||
self.session, self.configuration.token, "removed"
|
||||
):
|
||||
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 repo in repositories[category]:
|
||||
if repo in self.common.blacklist:
|
||||
if is_removed(repo):
|
||||
continue
|
||||
if self.is_known(repo):
|
||||
continue
|
||||
self.factory.tasks.append(
|
||||
self.factory.safe_register(self, repo, category)
|
||||
)
|
||||
self.factory.tasks.append(self.factory.safe_register(repo, category))
|
||||
await self.factory.execute()
|
||||
self.logger.info("Loading known repositories finished")
|
||||
|
45
config/custom_components/hacs/hacsbase/backup.py
Executable file → Normal file
45
config/custom_components/hacs/hacsbase/backup.py
Executable file → Normal file
@ -70,3 +70,48 @@ class Backup:
|
||||
while os.path.exists(self.backup_path):
|
||||
sleep(0.1)
|
||||
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")
|
||||
|
2
config/custom_components/hacs/hacsbase/configuration.py
Executable file → Normal file
2
config/custom_components/hacs/hacsbase/configuration.py
Executable file → Normal file
@ -11,6 +11,8 @@ class Configuration:
|
||||
# Main configuration:
|
||||
appdaemon_path: str = "appdaemon/apps/"
|
||||
appdaemon: bool = False
|
||||
netdaemon_path: str = "netdaemon/apps/"
|
||||
netdaemon: bool = False
|
||||
config: dict = {}
|
||||
config_entry: dict = {}
|
||||
config_type: str = None
|
||||
|
0
config/custom_components/hacs/hacsbase/const.py
Executable file → Normal file
0
config/custom_components/hacs/hacsbase/const.py
Executable file → Normal file
82
config/custom_components/hacs/hacsbase/data.py
Executable file → Normal file
82
config/custom_components/hacs/hacsbase/data.py
Executable file → Normal file
@ -1,50 +1,57 @@
|
||||
"""Data handler for HACS."""
|
||||
from integrationhelper import Logger
|
||||
from . import Hacs
|
||||
from ..const import VERSION
|
||||
from ..repositories.repository import HacsRepository
|
||||
from ..repositories.manifest import HacsManifest
|
||||
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."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.logger = Logger("hacs.data")
|
||||
self.hacs = get_hacs()
|
||||
|
||||
async def async_write(self):
|
||||
"""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
|
||||
|
||||
self.logger.debug("Saving data")
|
||||
|
||||
# Hacs
|
||||
await async_save_to_store(
|
||||
self.hass,
|
||||
self.hacs.hass,
|
||||
"hacs",
|
||||
{
|
||||
"view": self.configuration.frontend_mode,
|
||||
"compact": self.configuration.frontend_compact,
|
||||
"onboarding_done": self.configuration.onboarding_done,
|
||||
"view": self.hacs.configuration.frontend_mode,
|
||||
"compact": self.hacs.configuration.frontend_compact,
|
||||
"onboarding_done": self.hacs.configuration.onboarding_done,
|
||||
},
|
||||
)
|
||||
|
||||
await async_save_to_store(
|
||||
self.hacs.hass, "removed", [x.__dict__ for x in removed_repositories]
|
||||
)
|
||||
|
||||
# Repositories
|
||||
content = {}
|
||||
for repository in self.repositories:
|
||||
for repository in self.hacs.repositories:
|
||||
if repository.repository_manifest is not None:
|
||||
repository_manifest = repository.repository_manifest.manifest
|
||||
else:
|
||||
repository_manifest = None
|
||||
content[repository.information.uid] = {
|
||||
"authors": repository.information.authors,
|
||||
"category": repository.information.category,
|
||||
"description": repository.information.description,
|
||||
"downloads": repository.releases.last_release_object_downloads,
|
||||
"full_name": repository.information.full_name,
|
||||
"authors": repository.data.authors,
|
||||
"category": repository.data.category,
|
||||
"description": repository.data.description,
|
||||
"downloads": repository.releases.downloads,
|
||||
"full_name": repository.data.full_name,
|
||||
"first_install": repository.status.first_install,
|
||||
"hide": repository.status.hide,
|
||||
"installed_commit": repository.versions.installed_commit,
|
||||
@ -52,54 +59,56 @@ class HacsData(Hacs):
|
||||
"last_commit": repository.versions.available_commit,
|
||||
"last_release_tag": repository.versions.available,
|
||||
"last_updated": repository.information.last_updated,
|
||||
"name": repository.information.name,
|
||||
"name": repository.data.name,
|
||||
"new": repository.status.new,
|
||||
"repository_manifest": repository_manifest,
|
||||
"selected_tag": repository.status.selected_tag,
|
||||
"show_beta": repository.status.show_beta,
|
||||
"stars": repository.information.stars,
|
||||
"topics": repository.information.topics,
|
||||
"stars": repository.data.stargazers_count,
|
||||
"topics": repository.data.topics,
|
||||
"version_installed": repository.versions.installed,
|
||||
}
|
||||
|
||||
await async_save_to_store(self.hass, "repositories", content)
|
||||
self.hass.bus.async_fire("hacs/repository", {})
|
||||
self.hass.bus.fire("hacs/config", {})
|
||||
await async_save_to_store(self.hacs.hass, "repositories", content)
|
||||
self.hacs.hass.bus.async_fire("hacs/repository", {})
|
||||
self.hacs.hass.bus.fire("hacs/config", {})
|
||||
|
||||
async def restore(self):
|
||||
"""Restore saved data."""
|
||||
hacs = await async_load_from_store(self.hass, "hacs")
|
||||
repositories = await async_load_from_store(self.hass, "repositories")
|
||||
hacs = await async_load_from_store(self.hacs.hass, "hacs")
|
||||
repositories = await async_load_from_store(self.hacs.hass, "repositories")
|
||||
removed = await async_load_from_store(self.hacs.hass, "removed")
|
||||
try:
|
||||
if not hacs and not repositories:
|
||||
# Assume new install
|
||||
self.system.status.new = True
|
||||
self.hacs.system.status.new = True
|
||||
return True
|
||||
self.logger.info("Restore started")
|
||||
|
||||
# Hacs
|
||||
self.configuration.frontend_mode = hacs.get("view", "Grid")
|
||||
self.configuration.frontend_compact = hacs.get("compact", False)
|
||||
self.configuration.onboarding_done = hacs.get("onboarding_done", False)
|
||||
self.hacs.configuration.frontend_mode = hacs.get("view", "Grid")
|
||||
self.hacs.configuration.frontend_compact = hacs.get("compact", 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
|
||||
for entry in repositories:
|
||||
repo = repositories[entry]
|
||||
if repo["full_name"] == "hacs/integration":
|
||||
# Skip the old repo location
|
||||
continue
|
||||
if not self.is_known(repo["full_name"]):
|
||||
await self.register_repository(
|
||||
if not self.hacs.is_known(repo["full_name"]):
|
||||
await register_repository(
|
||||
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:
|
||||
self.logger.error(f"Did not find {repo['full_name']}")
|
||||
continue
|
||||
|
||||
# Restore repository attributes
|
||||
repository.information.uid = entry
|
||||
await self.hass.async_add_executor_job(
|
||||
await self.hacs.hass.async_add_executor_job(
|
||||
restore_repository_data, repository, repo
|
||||
)
|
||||
|
||||
@ -114,13 +123,12 @@ def restore_repository_data(
|
||||
repository: type(HacsRepository), repository_data: dict
|
||||
) -> None:
|
||||
"""Restore Repository Data"""
|
||||
repository.information.authors = repository_data.get("authors", [])
|
||||
repository.information.description = repository_data.get("description")
|
||||
repository.information.name = repository_data.get("name")
|
||||
repository.data.authors = repository_data.get("authors", [])
|
||||
repository.data.description = repository_data.get("description")
|
||||
repository.releases.last_release_object_downloads = repository_data.get("downloads")
|
||||
repository.information.last_updated = repository_data.get("last_updated")
|
||||
repository.information.topics = repository_data.get("topics", [])
|
||||
repository.information.stars = repository_data.get("stars", 0)
|
||||
repository.data.topics = repository_data.get("topics", [])
|
||||
repository.data.stargazers_count = repository_data.get("stars", 0)
|
||||
repository.releases.last_release = repository_data.get("last_release_tag")
|
||||
repository.status.hide = repository_data.get("hide", False)
|
||||
repository.status.installed = repository_data.get("installed", False)
|
||||
|
4
config/custom_components/hacs/hacsbase/exceptions.py
Executable file → Normal file
4
config/custom_components/hacs/hacsbase/exceptions.py
Executable file → Normal file
@ -3,3 +3,7 @@
|
||||
|
||||
class HacsException(Exception):
|
||||
"""Super basic."""
|
||||
|
||||
|
||||
class HacsExpectedException(HacsException):
|
||||
"""For stuff that are expected."""
|
||||
|
20
config/custom_components/hacs/hacsbase/task_factory.py
Executable file → Normal file
20
config/custom_components/hacs/hacsbase/task_factory.py
Executable file → Normal file
@ -5,6 +5,10 @@ from datetime import timedelta
|
||||
import asyncio
|
||||
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)
|
||||
sleeper = 5
|
||||
|
||||
@ -44,8 +48,8 @@ class HacsTaskFactory:
|
||||
async with max_concurrent_tasks:
|
||||
try:
|
||||
await repository.common_update()
|
||||
except AIOGitHubException as exception:
|
||||
logger.error(exception)
|
||||
except (AIOGitHubException, HacsException) as exception:
|
||||
logger.error("%s - %s", repository.data.full_name, exception)
|
||||
|
||||
# Due to GitHub ratelimits we need to sleep a bit
|
||||
await asyncio.sleep(sleeper)
|
||||
@ -54,18 +58,18 @@ class HacsTaskFactory:
|
||||
async with max_concurrent_tasks:
|
||||
try:
|
||||
await repository.update_repository()
|
||||
except AIOGitHubException as exception:
|
||||
logger.error(exception)
|
||||
except (AIOGitHubException, HacsException) as exception:
|
||||
logger.error("%s - %s", repository.data.full_name, exception)
|
||||
|
||||
# Due to GitHub ratelimits we need to sleep a bit
|
||||
await asyncio.sleep(sleeper)
|
||||
|
||||
async def safe_register(self, hacs, repo, category):
|
||||
async def safe_register(self, repo, category):
|
||||
async with max_concurrent_tasks:
|
||||
try:
|
||||
await hacs.register_repository(repo, category)
|
||||
except AIOGitHubException as exception:
|
||||
logger.error(exception)
|
||||
await register_repository(repo, category)
|
||||
except (AIOGitHubException, HacsException) as exception:
|
||||
logger.error("%s - %s", repo, exception)
|
||||
|
||||
# Due to GitHub ratelimits we need to sleep a bit
|
||||
await asyncio.sleep(sleeper)
|
||||
|
0
config/custom_components/hacs/handler/__init__.py
Executable file → Normal file
0
config/custom_components/hacs/handler/__init__.py
Executable file → Normal file
10
config/custom_components/hacs/handler/download.py
Executable file → Normal file
10
config/custom_components/hacs/handler/download.py
Executable file → Normal file
@ -7,15 +7,17 @@ import aiofiles
|
||||
import async_timeout
|
||||
from integrationhelper import Logger
|
||||
import backoff
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from ..hacsbase.exceptions import HacsException
|
||||
|
||||
from custom_components.hacs.globals import get_hacs
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
hacs = get_hacs()
|
||||
logger = Logger("hacs.download.downloader")
|
||||
if url is None:
|
||||
return
|
||||
@ -28,8 +30,8 @@ async def async_download_file(hass, url):
|
||||
|
||||
result = None
|
||||
|
||||
with async_timeout.timeout(60, loop=hass.loop):
|
||||
request = await async_get_clientsession(hass).get(url)
|
||||
with async_timeout.timeout(60, loop=hacs.hass.loop):
|
||||
request = await hacs.session.get(url)
|
||||
|
||||
# Make sure that we got a valid result
|
||||
if request.status == 200:
|
||||
|
0
config/custom_components/hacs/handler/template.py
Executable file → Normal file
0
config/custom_components/hacs/handler/template.py
Executable file → Normal file
124
config/custom_components/hacs/helpers/download.py
Executable file → Normal file
124
config/custom_components/hacs/helpers/download.py
Executable file → Normal file
@ -16,9 +16,13 @@ class FileInformation:
|
||||
|
||||
def should_try_releases(repository):
|
||||
"""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
|
||||
if repository.information.category not in ["plugin", "theme"]:
|
||||
if repository.data.category not in ["plugin", "theme"]:
|
||||
return False
|
||||
if not repository.releases.releases:
|
||||
return False
|
||||
@ -31,7 +35,7 @@ def gather_files_to_download(repository):
|
||||
tree = repository.tree
|
||||
ref = f"{repository.ref}".replace("tags/", "")
|
||||
releaseobjects = repository.releases.objects
|
||||
category = repository.information.category
|
||||
category = repository.data.category
|
||||
remotelocation = repository.content.path.remote
|
||||
|
||||
if should_try_releases(repository):
|
||||
@ -44,7 +48,7 @@ def gather_files_to_download(repository):
|
||||
|
||||
if repository.content.single:
|
||||
for treefile in tree:
|
||||
if treefile.filename == repository.information.file_name:
|
||||
if treefile.filename == repository.data.file_name:
|
||||
files.append(
|
||||
FileInformation(
|
||||
treefile.download_url, treefile.full_path, treefile.filename
|
||||
@ -55,28 +59,29 @@ def gather_files_to_download(repository):
|
||||
if category == "plugin":
|
||||
for treefile in tree:
|
||||
if treefile.path in ["", "dist"]:
|
||||
if not remotelocation:
|
||||
if treefile.filename != repository.information.file_name:
|
||||
continue
|
||||
if remotelocation == "dist" and not treefile.filename.startswith(
|
||||
"dist"
|
||||
):
|
||||
continue
|
||||
if treefile.is_directory:
|
||||
continue
|
||||
files.append(
|
||||
FileInformation(
|
||||
treefile.download_url, treefile.full_path, treefile.filename
|
||||
if not remotelocation:
|
||||
if not treefile.filename.endswith(".js"):
|
||||
continue
|
||||
if treefile.path != "":
|
||||
continue
|
||||
if not treefile.is_directory:
|
||||
files.append(
|
||||
FileInformation(
|
||||
treefile.download_url, treefile.full_path, treefile.filename
|
||||
)
|
||||
)
|
||||
)
|
||||
if files:
|
||||
return files
|
||||
|
||||
if repository.repository_manifest.content_in_root:
|
||||
if repository.repository_manifest.filename is None:
|
||||
if repository.data.content_in_root:
|
||||
if not repository.data.filename:
|
||||
if category == "theme":
|
||||
tree = filter_content_return_one_of_type(
|
||||
repository.tree, "themes", "yaml", "full_path"
|
||||
repository.tree, "", "yaml", "full_path"
|
||||
)
|
||||
|
||||
for path in tree:
|
||||
@ -104,21 +109,17 @@ async def download_zip(repository, validate):
|
||||
return validate
|
||||
|
||||
for content in contents:
|
||||
filecontent = await async_download_file(
|
||||
repository.hass, content.download_url
|
||||
)
|
||||
filecontent = await async_download_file(content.download_url)
|
||||
|
||||
if filecontent is None:
|
||||
validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
continue
|
||||
|
||||
result = await async_save_file(
|
||||
f"{tempfile.gettempdir()}/{repository.repository_manifest.filename}",
|
||||
filecontent,
|
||||
f"{tempfile.gettempdir()}/{repository.data.filename}", filecontent
|
||||
)
|
||||
with zipfile.ZipFile(
|
||||
f"{tempfile.gettempdir()}/{repository.repository_manifest.filename}",
|
||||
"r",
|
||||
f"{tempfile.gettempdir()}/{repository.data.filename}", "r"
|
||||
) as zip_file:
|
||||
zip_file.extractall(repository.content.path.local)
|
||||
|
||||
@ -132,54 +133,49 @@ async def download_zip(repository, validate):
|
||||
return validate
|
||||
|
||||
|
||||
async def download_content(repository, validate, local_directory):
|
||||
async def download_content(repository):
|
||||
"""Download the content of a directory."""
|
||||
contents = gather_files_to_download(repository)
|
||||
try:
|
||||
if not contents:
|
||||
raise HacsException("No content to download")
|
||||
repository.logger.debug(repository.data.filename)
|
||||
if not contents:
|
||||
raise HacsException("No content to download")
|
||||
|
||||
for content in contents:
|
||||
if repository.repository_manifest.content_in_root:
|
||||
if repository.repository_manifest.filename is not None:
|
||||
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.")
|
||||
for content in contents:
|
||||
if repository.data.content_in_root and repository.data.filename:
|
||||
if content.name != repository.data.filename:
|
||||
continue
|
||||
repository.logger.debug(f"Downloading {content.name}")
|
||||
|
||||
# Save the content of the file.
|
||||
if repository.content.single or content.path is None:
|
||||
local_directory = repository.content.path.local
|
||||
filecontent = await async_download_file(content.download_url)
|
||||
|
||||
else:
|
||||
_content_path = content.path
|
||||
if not repository.repository_manifest.content_in_root:
|
||||
_content_path = _content_path.replace(
|
||||
f"{repository.content.path.remote}", ""
|
||||
)
|
||||
if filecontent is None:
|
||||
repository.validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
continue
|
||||
|
||||
local_directory = f"{repository.content.path.local}/{_content_path}"
|
||||
local_directory = local_directory.split("/")
|
||||
del local_directory[-1]
|
||||
local_directory = "/".join(local_directory)
|
||||
# Save the content of the file.
|
||||
if repository.content.single or content.path is None:
|
||||
local_directory = repository.content.path.local
|
||||
|
||||
# Check local directory
|
||||
pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
_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}"
|
||||
result = await async_save_file(local_file_path, filecontent)
|
||||
if result:
|
||||
repository.logger.info(f"download of {content.name} complete")
|
||||
continue
|
||||
validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
local_directory = f"{repository.content.path.local}/{_content_path}"
|
||||
local_directory = local_directory.split("/")
|
||||
del local_directory[-1]
|
||||
local_directory = "/".join(local_directory)
|
||||
|
||||
# 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
11
config/custom_components/hacs/helpers/filters.py
Executable file → Normal file
@ -42,3 +42,14 @@ def find_first_of_filetype(content, filterfiltype, attr="name"):
|
||||
filename = getattr(_filename, attr)
|
||||
break
|
||||
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
5
config/custom_components/hacs/helpers/get_defaults.py
Executable file → Normal file
@ -2,6 +2,7 @@
|
||||
import json
|
||||
from aiogithubapi import AIOGitHub, AIOGitHubException
|
||||
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:
|
||||
@ -27,13 +28,13 @@ async def get_default_repos_orgs(github: type(AIOGitHub), category: str) -> dict
|
||||
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."""
|
||||
repositories = []
|
||||
logger = Logger("hacs")
|
||||
|
||||
try:
|
||||
repo = await github.get_repo("hacs/default")
|
||||
repo = await get_repository(session, token, "hacs/default")
|
||||
content = await repo.get_contents(default)
|
||||
repositories = json.loads(content.content)
|
||||
|
||||
|
184
config/custom_components/hacs/helpers/information.py
Normal file
184
config/custom_components/hacs/helpers/information.py
Normal 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
92
config/custom_components/hacs/helpers/install.py
Executable file → Normal file
@ -1,14 +1,17 @@
|
||||
"""Install helper for repositories."""
|
||||
import os
|
||||
import tempfile
|
||||
from custom_components.hacs.globals import get_hacs
|
||||
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):
|
||||
"""Common installation steps of the repository."""
|
||||
persistent_directory = None
|
||||
await repository.update_repository()
|
||||
repository.validate.errors = []
|
||||
|
||||
if not repository.can_install:
|
||||
raise HacsException(
|
||||
@ -16,41 +19,36 @@ async def install_repository(repository):
|
||||
)
|
||||
|
||||
version = version_to_install(repository)
|
||||
if version == repository.information.default_branch:
|
||||
if version == repository.data.default_branch:
|
||||
repository.ref = version
|
||||
else:
|
||||
repository.ref = f"tags/{version}"
|
||||
|
||||
if repository.repository_manifest:
|
||||
if repository.repository_manifest.persistent_directory:
|
||||
if os.path.exists(
|
||||
f"{repository.content.path.local}/{repository.repository_manifest.persistent_directory}"
|
||||
):
|
||||
persistent_directory = Backup(
|
||||
f"{repository.content.path.local}/{repository.repository_manifest.persistent_directory}",
|
||||
tempfile.gettempdir() + "/hacs_persistent_directory/",
|
||||
)
|
||||
persistent_directory.create()
|
||||
if repository.status.installed and repository.data.category == "netdaemon":
|
||||
persistent_directory = BackupNetDaemon(repository)
|
||||
persistent_directory.create()
|
||||
|
||||
elif repository.data.persistent_directory:
|
||||
if os.path.exists(
|
||||
f"{repository.content.path.local}/{repository.data.persistent_directory}"
|
||||
):
|
||||
persistent_directory = Backup(
|
||||
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:
|
||||
backup = Backup(repository.content.path.local)
|
||||
backup.create()
|
||||
|
||||
if (
|
||||
repository.repository_manifest.zip_release
|
||||
and version != repository.information.default_branch
|
||||
):
|
||||
validate = await repository.download_zip(repository.validate)
|
||||
if repository.data.zip_release and version != repository.data.default_branch:
|
||||
await repository.download_zip(repository)
|
||||
else:
|
||||
validate = await repository.download_content(
|
||||
repository.validate,
|
||||
repository.content.path.remote,
|
||||
repository.content.path.local,
|
||||
repository.ref,
|
||||
)
|
||||
await download_content(repository)
|
||||
|
||||
if validate.errors:
|
||||
for error in validate.errors:
|
||||
if repository.validate.errors:
|
||||
for error in repository.validate.errors:
|
||||
repository.logger.error(error)
|
||||
if repository.status.installed and not repository.content.single:
|
||||
backup.restore()
|
||||
@ -62,14 +60,14 @@ async def install_repository(repository):
|
||||
persistent_directory.restore()
|
||||
persistent_directory.cleanup()
|
||||
|
||||
if validate.success:
|
||||
if repository.information.full_name not in repository.common.installed:
|
||||
if repository.information.full_name == "hacs/integration":
|
||||
repository.common.installed.append(repository.information.full_name)
|
||||
if repository.validate.success:
|
||||
if repository.data.full_name not in repository.hacs.common.installed:
|
||||
if repository.data.full_name == "hacs/integration":
|
||||
repository.hacs.common.installed.append(repository.data.full_name)
|
||||
repository.status.installed = True
|
||||
repository.versions.installed_commit = repository.versions.available_commit
|
||||
|
||||
if version == repository.information.default_branch:
|
||||
if version == repository.data.default_branch:
|
||||
repository.versions.installed = None
|
||||
else:
|
||||
repository.versions.installed = version
|
||||
@ -80,28 +78,34 @@ async def install_repository(repository):
|
||||
|
||||
async def reload_after_install(repository):
|
||||
"""Reload action after installation success."""
|
||||
if repository.information.category == "integration":
|
||||
if repository.data.category == "integration":
|
||||
if repository.config_flow:
|
||||
if repository.information.full_name != "hacs/integration":
|
||||
if repository.data.full_name != "hacs/integration":
|
||||
await repository.reload_custom_components()
|
||||
repository.pending_restart = True
|
||||
|
||||
elif repository.information.category == "theme":
|
||||
elif repository.data.category == "theme":
|
||||
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
|
||||
pass
|
||||
|
||||
|
||||
def installation_complete(repository):
|
||||
"""Action to run when the installation is complete."""
|
||||
repository.hass.bus.async_fire(
|
||||
hacs = get_hacs()
|
||||
hacs.hass.bus.async_fire(
|
||||
"hacs/repository",
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "install",
|
||||
"repository": repository.information.full_name,
|
||||
},
|
||||
{"id": 1337, "action": "install", "repository": repository.data.full_name},
|
||||
)
|
||||
|
||||
|
||||
@ -115,10 +119,10 @@ def version_to_install(repository):
|
||||
return repository.status.selected_tag
|
||||
return repository.versions.available
|
||||
if repository.status.selected_tag is not None:
|
||||
if repository.status.selected_tag == repository.information.default_branch:
|
||||
return repository.information.default_branch
|
||||
if repository.status.selected_tag == repository.data.default_branch:
|
||||
return repository.data.default_branch
|
||||
if repository.status.selected_tag in repository.releases.published_tags:
|
||||
return repository.status.selected_tag
|
||||
if repository.information.default_branch is None:
|
||||
if repository.data.default_branch is None:
|
||||
return "master"
|
||||
return repository.information.default_branch
|
||||
return repository.data.default_branch
|
||||
|
23
config/custom_components/hacs/helpers/misc.py
Executable file → Normal file
23
config/custom_components/hacs/helpers/misc.py
Executable file → Normal file
@ -2,20 +2,23 @@
|
||||
import semantic_version
|
||||
|
||||
|
||||
def get_repository_name(
|
||||
hacs_manifest, repository_name: str, category: str = None, manifest: dict = None
|
||||
) -> str:
|
||||
def get_repository_name(repository) -> str:
|
||||
"""Return the name of the repository for use in the frontend."""
|
||||
|
||||
if hacs_manifest.name is not None:
|
||||
return hacs_manifest.name
|
||||
if repository.repository_manifest.name is not None:
|
||||
return repository.repository_manifest.name
|
||||
|
||||
if category == "integration":
|
||||
if manifest:
|
||||
if "name" in manifest:
|
||||
return manifest["name"]
|
||||
if repository.data.category == "integration":
|
||||
if repository.integration_manifest:
|
||||
if "name" in repository.integration_manifest:
|
||||
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:
|
||||
|
8
config/custom_components/hacs/helpers/network.py
Normal file
8
config/custom_components/hacs/helpers/network.py
Normal 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
|
49
config/custom_components/hacs/helpers/register_repository.py
Normal file
49
config/custom_components/hacs/helpers/register_repository.py
Normal 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)
|
90
config/custom_components/hacs/helpers/validate_repository.py
Normal file
90
config/custom_components/hacs/helpers/validate_repository.py
Normal 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
106
config/custom_components/hacs/http.py
Executable file → Normal file
@ -1,13 +1,14 @@
|
||||
"""HACS http endpoints."""
|
||||
import os
|
||||
from integrationhelper import Logger
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from aiohttp import web
|
||||
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."""
|
||||
|
||||
requires_auth = False
|
||||
@ -16,50 +17,67 @@ class HacsFrontend(HomeAssistantView, Hacs):
|
||||
|
||||
async def get(self, request, requested_file): # pylint: disable=unused-argument
|
||||
"""Handle HACS Web requests."""
|
||||
|
||||
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)
|
||||
return await get_file_response(requested_file)
|
||||
|
||||
|
||||
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:.+}"
|
||||
|
||||
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
0
config/custom_components/hacs/iconset.js
Executable file → Normal file
5
config/custom_components/hacs/manifest.json
Executable file → Normal file
5
config/custom_components/hacs/manifest.json
Executable file → Normal file
@ -9,14 +9,15 @@
|
||||
"persistent_notification",
|
||||
"lovelace"
|
||||
],
|
||||
"documentation": "https://hacs.xyz",
|
||||
"documentation": "https://hacs.xyz/docs/configuration/start",
|
||||
"domain": "hacs",
|
||||
"issues": "https://hacs.xyz/docs/issues",
|
||||
"name": "HACS (Home Assistant Community Store)",
|
||||
"requirements": [
|
||||
"aiofiles==0.4.0",
|
||||
"aiogithubapi==0.5.0",
|
||||
"backoff==1.10.0",
|
||||
"hacs_frontend==20200212060803",
|
||||
"hacs_frontend==20200309184730",
|
||||
"integrationhelper==0.2.2",
|
||||
"semantic_version==2.8.4"
|
||||
]
|
||||
|
20
config/custom_components/hacs/repositories/__init__.py
Executable file → Normal file
20
config/custom_components/hacs/repositories/__init__.py
Executable file → Normal file
@ -1,6 +1,16 @@
|
||||
"""Initialize repositories."""
|
||||
from .theme import HacsTheme
|
||||
from .integration import HacsIntegration
|
||||
from .python_script import HacsPythonScript
|
||||
from .appdaemon import HacsAppdaemon
|
||||
from .plugin import HacsPlugin
|
||||
from custom_components.hacs.repositories.theme import HacsTheme
|
||||
from custom_components.hacs.repositories.integration import HacsIntegration
|
||||
from custom_components.hacs.repositories.python_script import HacsPythonScript
|
||||
from custom_components.hacs.repositories.appdaemon import HacsAppdaemon
|
||||
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
30
config/custom_components/hacs/repositories/appdaemon.py
Executable file → Normal file
@ -1,27 +1,27 @@
|
||||
"""Class for appdaemon apps in HACS."""
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from .repository import HacsRepository, register_repository_class
|
||||
from integrationhelper import Logger
|
||||
|
||||
from .repository import HacsRepository
|
||||
from ..hacsbase.exceptions import HacsException
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsAppdaemon(HacsRepository):
|
||||
"""Appdaemon apps in HACS."""
|
||||
|
||||
category = "appdaemon"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.data.full_name = full_name
|
||||
self.data.category = "appdaemon"
|
||||
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.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):
|
||||
"""Validate."""
|
||||
@ -39,19 +39,14 @@ class HacsAppdaemon(HacsRepository):
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
self.content.path.remote = addir[0].path
|
||||
self.information.name = addir[0].name
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
# Handle potential errors
|
||||
if 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)
|
||||
return self.validate.success
|
||||
|
||||
@ -68,13 +63,13 @@ class HacsAppdaemon(HacsRepository):
|
||||
|
||||
async def update_repository(self):
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
if self.hacs.github.ratelimits.remaining == 0:
|
||||
return
|
||||
await self.common_update()
|
||||
|
||||
# Get appdaemon objects.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
if self.data.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "apps":
|
||||
@ -82,14 +77,9 @@ class HacsAppdaemon(HacsRepository):
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
self.content.path.remote = addir[0].path
|
||||
self.information.name = addir[0].name
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
134
config/custom_components/hacs/repositories/integration.py
Executable file → Normal file
134
config/custom_components/hacs/repositories/integration.py
Executable file → Normal file
@ -1,80 +1,56 @@
|
||||
"""Class for integrations in HACS."""
|
||||
import json
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from integrationhelper import Logger
|
||||
|
||||
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):
|
||||
"""Integrations in HACS."""
|
||||
|
||||
category = "integration"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.domain = None
|
||||
self.data.full_name = full_name
|
||||
self.data.category = "integration"
|
||||
self.content.path.remote = "custom_components"
|
||||
self.content.path.local = self.localpath
|
||||
self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""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):
|
||||
"""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.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
if self.data.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "custom_components":
|
||||
try:
|
||||
ccdir = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
except AIOGitHubException:
|
||||
name = get_first_directory_in_directory(self.tree, "custom_components")
|
||||
if name is None:
|
||||
raise HacsException(
|
||||
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
self.content.path.remote = f"custom_components/{name}"
|
||||
|
||||
for item in ccdir or []:
|
||||
if item.type == "dir":
|
||||
self.content.path.remote = item.path
|
||||
break
|
||||
|
||||
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.")
|
||||
try:
|
||||
await get_integration_manifest(self)
|
||||
except HacsException as exception:
|
||||
self.logger.error(exception)
|
||||
|
||||
# Handle potential errors
|
||||
if 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)
|
||||
return self.validate.success
|
||||
|
||||
@ -86,46 +62,26 @@ class HacsIntegration(HacsRepository):
|
||||
# Run common registration steps.
|
||||
await self.common_registration()
|
||||
|
||||
# Get the content of the manifest file.
|
||||
await self.get_manifest()
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
async def update_repository(self):
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
if self.hacs.github.ratelimits.remaining == 0:
|
||||
return
|
||||
await self.common_update()
|
||||
|
||||
# Get integration objects.
|
||||
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
if self.data.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "custom_components":
|
||||
ccdir = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
if not isinstance(ccdir, list):
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
self.content.path.remote = ccdir[0].path
|
||||
name = get_first_directory_in_directory(self.tree, "custom_components")
|
||||
self.content.path.remote = f"custom_components/{name}"
|
||||
|
||||
try:
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
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()
|
||||
await get_integration_manifest(self)
|
||||
except HacsException as exception:
|
||||
self.logger.error(exception)
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
@ -133,33 +89,5 @@ class HacsIntegration(HacsRepository):
|
||||
async def reload_custom_components(self):
|
||||
"""Reload custom_components (and config flows)in HA."""
|
||||
self.logger.info("Reloading custom_component cache")
|
||||
del self.hass.data["custom_components"]
|
||||
await async_get_custom_components(self.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
|
||||
del self.hacs.hass.data["custom_components"]
|
||||
await async_get_custom_components(self.hacs.hass)
|
||||
|
0
config/custom_components/hacs/repositories/manifest.py
Executable file → Normal file
0
config/custom_components/hacs/repositories/manifest.py
Executable file → Normal file
90
config/custom_components/hacs/repositories/netdaemon.py
Normal file
90
config/custom_components/hacs/repositories/netdaemon.py
Normal 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
95
config/custom_components/hacs/repositories/plugin.py
Executable file → Normal file
@ -1,26 +1,27 @@
|
||||
"""Class for plugins in HACS."""
|
||||
import json
|
||||
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 custom_components.hacs.helpers.information import find_file_name
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsPlugin(HacsRepository):
|
||||
"""Plugins in HACS."""
|
||||
|
||||
category = "plugin"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.information.file_name = None
|
||||
self.data.full_name = full_name
|
||||
self.data.file_name = None
|
||||
self.data.category = "plugin"
|
||||
self.information.javascript_type = None
|
||||
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):
|
||||
"""Validate."""
|
||||
@ -28,7 +29,7 @@ class HacsPlugin(HacsRepository):
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
await self.get_plugin_location()
|
||||
find_file_name(self)
|
||||
|
||||
if self.content.path.remote is None:
|
||||
raise HacsException(
|
||||
@ -38,14 +39,10 @@ class HacsPlugin(HacsRepository):
|
||||
if self.content.path.remote == "release":
|
||||
self.content.single = True
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
# Handle potential errors
|
||||
if 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)
|
||||
return self.validate.success
|
||||
|
||||
@ -59,13 +56,13 @@ class HacsPlugin(HacsRepository):
|
||||
|
||||
async def update_repository(self):
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
if self.hacs.github.ratelimits.remaining == 0:
|
||||
return
|
||||
# Run common update steps.
|
||||
await self.common_update()
|
||||
|
||||
# Get plugin objects.
|
||||
await self.get_plugin_location()
|
||||
find_file_name(self)
|
||||
|
||||
# Get JS type
|
||||
await self.parse_readme_for_jstype()
|
||||
@ -76,68 +73,6 @@ class HacsPlugin(HacsRepository):
|
||||
if self.content.path.remote == "release":
|
||||
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):
|
||||
"""Get package content."""
|
||||
try:
|
||||
@ -145,7 +80,7 @@ class HacsPlugin(HacsRepository):
|
||||
package = json.loads(package.content)
|
||||
|
||||
if package:
|
||||
self.information.authors = package["author"]
|
||||
self.data.authors = package["author"]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
|
72
config/custom_components/hacs/repositories/python_script.py
Executable file → Normal file
72
config/custom_components/hacs/repositories/python_script.py
Executable file → Normal file
@ -1,10 +1,11 @@
|
||||
"""Class for python_scripts in HACS."""
|
||||
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 ..helpers.information import find_file_name
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsPythonScript(HacsRepository):
|
||||
"""python_scripts in HACS."""
|
||||
|
||||
@ -13,11 +14,12 @@ class HacsPythonScript(HacsRepository):
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.data.full_name = full_name
|
||||
self.data.category = "python_script"
|
||||
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.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
@ -25,26 +27,25 @@ class HacsPythonScript(HacsRepository):
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
try:
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
except AIOGitHubException:
|
||||
if self.data.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
compliant = False
|
||||
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(
|
||||
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
|
||||
if 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)
|
||||
return self.validate.success
|
||||
|
||||
@ -57,31 +58,30 @@ class HacsPythonScript(HacsRepository):
|
||||
await self.common_registration()
|
||||
|
||||
# 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]
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
if self.hacs.github.ratelimits.remaining == 0:
|
||||
return
|
||||
# Run common update steps.
|
||||
await self.common_update()
|
||||
|
||||
# Get python_script objects.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
if self.data.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
compliant = False
|
||||
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(
|
||||
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
# Update name
|
||||
self.information.name = self.content.objects[0].name.replace(".py", "")
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
find_file_name(self)
|
||||
|
17
config/custom_components/hacs/repositories/removed.py
Normal file
17
config/custom_components/hacs/repositories/removed.py
Normal 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
307
config/custom_components/hacs/repositories/repository.py
Executable file → Normal file
@ -1,29 +1,27 @@
|
||||
"""Repository."""
|
||||
# pylint: disable=broad-except, bad-continuation, no-member
|
||||
import pathlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from integrationhelper import Validate, Logger
|
||||
from integrationhelper import Validate
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from .manifest import HacsManifest
|
||||
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 ..helpers.misc import version_left_higher_then_right
|
||||
from ..helpers.install import install_repository, version_to_install
|
||||
|
||||
|
||||
RERPOSITORY_CLASSES = {}
|
||||
|
||||
|
||||
def register_repository_class(cls):
|
||||
"""Register class."""
|
||||
RERPOSITORY_CLASSES[cls.category] = cls
|
||||
return cls
|
||||
from custom_components.hacs.globals import get_hacs
|
||||
from custom_components.hacs.helpers.information import (
|
||||
get_info_md_content,
|
||||
get_repository,
|
||||
)
|
||||
from custom_components.hacs.helpers.validate_repository import (
|
||||
common_validate,
|
||||
common_update_data,
|
||||
)
|
||||
from custom_components.hacs.repositories.repositorydata import RepositoryData
|
||||
|
||||
|
||||
class RepositoryVersions:
|
||||
@ -79,6 +77,7 @@ class RepositoryReleases:
|
||||
published_tags = []
|
||||
objects = []
|
||||
releases = False
|
||||
downloads = None
|
||||
|
||||
|
||||
class RepositoryPath:
|
||||
@ -97,26 +96,25 @@ class RepositoryContent:
|
||||
single = False
|
||||
|
||||
|
||||
class HacsRepository(Hacs):
|
||||
class HacsRepository:
|
||||
"""HacsRepository."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set up HacsRepository."""
|
||||
|
||||
self.data = {}
|
||||
self.hacs = get_hacs()
|
||||
self.data = RepositoryData()
|
||||
self.content = RepositoryContent()
|
||||
self.content.path = RepositoryPath()
|
||||
self.information = RepositoryInformation()
|
||||
self.repository_object = None
|
||||
self.status = RepositoryStatus()
|
||||
self.state = None
|
||||
self.manifest = {}
|
||||
self.integration_manifest = {}
|
||||
self.repository_manifest = HacsManifest.from_dict({})
|
||||
self.validate = Validate()
|
||||
self.releases = RepositoryReleases()
|
||||
self.versions = RepositoryVersions()
|
||||
self.pending_restart = False
|
||||
self.logger = None
|
||||
self.tree = []
|
||||
self.treefiles = []
|
||||
self.ref = None
|
||||
@ -126,7 +124,7 @@ class HacsRepository(Hacs):
|
||||
"""Return pending upgrade."""
|
||||
if self.status.installed:
|
||||
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:
|
||||
return True
|
||||
return False
|
||||
@ -137,23 +135,20 @@ class HacsRepository(Hacs):
|
||||
@property
|
||||
def config_flow(self):
|
||||
"""Return bool if integration has config_flow."""
|
||||
if self.manifest:
|
||||
if self.information.full_name == "hacs/integration":
|
||||
if self.integration_manifest:
|
||||
if self.data.full_name == "hacs/integration":
|
||||
return False
|
||||
return self.manifest.get("config_flow", False)
|
||||
return self.integration_manifest.get("config_flow", False)
|
||||
return False
|
||||
|
||||
@property
|
||||
def custom(self):
|
||||
"""Return flag if the repository is custom."""
|
||||
if self.information.full_name.split("/")[0] in [
|
||||
"custom-components",
|
||||
"custom-cards",
|
||||
]:
|
||||
if self.data.full_name.split("/")[0] in ["custom-components", "custom-cards"]:
|
||||
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
|
||||
if self.information.full_name == "hacs/integration":
|
||||
if self.data.full_name == "hacs/integration":
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -164,24 +159,21 @@ class HacsRepository(Hacs):
|
||||
if self.information.homeassistant_version is not None:
|
||||
target = self.information.homeassistant_version
|
||||
if self.repository_manifest is not None:
|
||||
if self.repository_manifest.homeassistant is not None:
|
||||
target = self.repository_manifest.homeassistant
|
||||
if self.data.homeassistant is not None:
|
||||
target = self.data.homeassistant
|
||||
|
||||
if target is not None:
|
||||
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 True
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Return display name."""
|
||||
return get_repository_name(
|
||||
self.repository_manifest,
|
||||
self.information.name,
|
||||
self.information.category,
|
||||
self.manifest,
|
||||
)
|
||||
return get_repository_name(self)
|
||||
|
||||
@property
|
||||
def display_status(self):
|
||||
@ -257,127 +249,41 @@ class HacsRepository(Hacs):
|
||||
|
||||
async def common_validate(self):
|
||||
"""Common validation steps of the repository."""
|
||||
# Attach helpers
|
||||
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]
|
||||
await common_validate(self)
|
||||
|
||||
async def common_registration(self):
|
||||
"""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
|
||||
if self.repository_object is None:
|
||||
self.repository_object = await self.github.get_repo(
|
||||
self.information.full_name
|
||||
self.repository_object = await get_repository(
|
||||
self.hacs.session, self.hacs.configuration.token, self.data.full_name
|
||||
)
|
||||
self.data.update_data(self.repository_object.attributes)
|
||||
|
||||
# Set id
|
||||
self.information.uid = str(self.repository_object.id)
|
||||
self.information.uid = str(self.data.id)
|
||||
|
||||
# Set topics
|
||||
self.information.topics = self.repository_object.topics
|
||||
self.data.topics = self.data.topics
|
||||
|
||||
# Set stargazers_count
|
||||
self.information.stars = self.repository_object.attributes.get(
|
||||
"stargazers_count", 0
|
||||
)
|
||||
self.data.stargazers_count = self.data.stargazers_count
|
||||
|
||||
# Set description
|
||||
if self.repository_object.description:
|
||||
self.information.description = self.repository_object.description
|
||||
self.data.description = self.data.description
|
||||
|
||||
async def common_update(self):
|
||||
"""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")
|
||||
|
||||
# Set ref
|
||||
if self.ref is None:
|
||||
self.ref = version_to_install(self)
|
||||
|
||||
# Attach repository
|
||||
self.repository_object = await self.github.get_repo(self.information.full_name)
|
||||
|
||||
# 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
|
||||
await common_update_data(self)
|
||||
|
||||
# Update last updaeted
|
||||
self.information.last_updated = self.repository_object.attributes.get(
|
||||
"pushed_at", 0
|
||||
)
|
||||
|
||||
# Update topics
|
||||
self.information.topics = self.repository_object.topics
|
||||
|
||||
# Update last available commit
|
||||
await self.repository_object.set_last_commit()
|
||||
self.versions.available_commit = self.repository_object.last_commit
|
||||
@ -386,10 +292,7 @@ class HacsRepository(Hacs):
|
||||
await self.get_repository_manifest_content()
|
||||
|
||||
# Update "info.md"
|
||||
await self.get_info_md_content()
|
||||
|
||||
# Update releases
|
||||
await self.get_releases()
|
||||
self.information.additional_info = await get_info_md_content(self)
|
||||
|
||||
async def install(self):
|
||||
"""Common installation steps of the repository."""
|
||||
@ -409,18 +312,17 @@ class HacsRepository(Hacs):
|
||||
return validate
|
||||
|
||||
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:
|
||||
validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
continue
|
||||
|
||||
result = await async_save_file(
|
||||
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}",
|
||||
filecontent,
|
||||
f"{tempfile.gettempdir()}/{self.data.filename}", filecontent
|
||||
)
|
||||
with zipfile.ZipFile(
|
||||
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", "r"
|
||||
f"{tempfile.gettempdir()}/{self.data.filename}", "r"
|
||||
) as zip_file:
|
||||
zip_file.extractall(self.content.path.local)
|
||||
|
||||
@ -437,11 +339,13 @@ class HacsRepository(Hacs):
|
||||
"""Download the content of a directory."""
|
||||
from custom_components.hacs.helpers.download import download_content
|
||||
|
||||
validate = await download_content(self, validate, local_directory)
|
||||
validate = await download_content(self)
|
||||
return validate
|
||||
|
||||
async def get_repository_manifest_content(self):
|
||||
"""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:
|
||||
self.ref = version_to_install(self)
|
||||
try:
|
||||
@ -449,125 +353,44 @@ class HacsRepository(Hacs):
|
||||
self.repository_manifest = HacsManifest.from_dict(
|
||||
json.loads(manifest.content)
|
||||
)
|
||||
self.data.update_data(json.loads(manifest.content))
|
||||
except (AIOGitHubException, Exception): # Gotta Catch 'Em All
|
||||
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):
|
||||
"""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")
|
||||
|
||||
if self.information.uid in self.common.installed:
|
||||
self.common.installed.remove(self.information.uid)
|
||||
for repository in self.repositories:
|
||||
if self.information.uid in self.hacs.common.installed:
|
||||
self.hacs.common.installed.remove(self.information.uid)
|
||||
for repository in self.hacs.repositories:
|
||||
if repository.information.uid == self.information.uid:
|
||||
self.repositories.remove(repository)
|
||||
self.hacs.repositories.remove(repository)
|
||||
|
||||
async def uninstall(self):
|
||||
"""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")
|
||||
await self.remove_local_directory()
|
||||
self.status.installed = False
|
||||
if self.information.category == "integration":
|
||||
if self.data.category == "integration":
|
||||
if self.config_flow:
|
||||
await self.reload_custom_components()
|
||||
else:
|
||||
self.pending_restart = True
|
||||
elif self.information.category == "theme":
|
||||
elif self.data.category == "theme":
|
||||
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
|
||||
pass
|
||||
if self.information.full_name in self.common.installed:
|
||||
self.common.installed.remove(self.information.full_name)
|
||||
if self.data.full_name in self.hacs.common.installed:
|
||||
self.hacs.common.installed.remove(self.data.full_name)
|
||||
self.versions.installed = None
|
||||
self.versions.installed_commit = None
|
||||
self.hass.bus.async_fire(
|
||||
self.hacs.hass.bus.async_fire(
|
||||
"hacs/repository",
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "uninstall",
|
||||
"repository": self.information.full_name,
|
||||
},
|
||||
{"id": 1337, "action": "uninstall", "repository": self.data.full_name},
|
||||
)
|
||||
|
||||
async def remove_local_directory(self):
|
||||
@ -576,13 +399,11 @@ class HacsRepository(Hacs):
|
||||
from asyncio import sleep
|
||||
|
||||
try:
|
||||
if self.information.category == "python_script":
|
||||
local_path = "{}/{}.py".format(
|
||||
self.content.path.local, self.information.name
|
||||
)
|
||||
elif self.information.category == "theme":
|
||||
if self.data.category == "python_script":
|
||||
local_path = "{}/{}.py".format(self.content.path.local, self.data.name)
|
||||
elif self.data.category == "theme":
|
||||
local_path = "{}/{}.yaml".format(
|
||||
self.content.path.local, self.information.name
|
||||
self.content.path.local, self.data.name
|
||||
)
|
||||
else:
|
||||
local_path = self.content.path.local
|
||||
@ -590,7 +411,7 @@ class HacsRepository(Hacs):
|
||||
if os.path.exists(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)
|
||||
else:
|
||||
shutil.rmtree(local_path)
|
||||
|
82
config/custom_components/hacs/repositories/repositorydata.py
Normal file
82
config/custom_components/hacs/repositories/repositorydata.py
Normal 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
72
config/custom_components/hacs/repositories/theme.py
Executable file → Normal file
@ -1,23 +1,22 @@
|
||||
"""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 ..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):
|
||||
"""Themes in HACS."""
|
||||
|
||||
category = "theme"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.data.full_name = full_name
|
||||
self.data.category = "theme"
|
||||
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.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}")
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
@ -27,7 +26,6 @@ class HacsTheme(HacsRepository):
|
||||
# Custom step 1: Validate content.
|
||||
compliant = False
|
||||
for treefile in self.treefiles:
|
||||
self.logger.debug(treefile)
|
||||
if treefile.startswith("themes/") and treefile.endswith(".yaml"):
|
||||
compliant = True
|
||||
break
|
||||
@ -36,25 +34,13 @@ class HacsTheme(HacsRepository):
|
||||
f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
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"
|
||||
)
|
||||
if self.data.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
# Handle potential errors
|
||||
if 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)
|
||||
return self.validate.success
|
||||
|
||||
@ -67,44 +53,20 @@ class HacsTheme(HacsRepository):
|
||||
await self.common_registration()
|
||||
|
||||
# Set name
|
||||
if self.repository_manifest.filename is not None:
|
||||
self.information.file_name = self.repository_manifest.filename
|
||||
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}"
|
||||
)
|
||||
find_file_name(self)
|
||||
self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"
|
||||
|
||||
async def update_repository(self): # lgtm[py/similar-function]
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
if self.hacs.github.ratelimits.remaining == 0:
|
||||
return
|
||||
# Run common update steps.
|
||||
await self.common_update()
|
||||
|
||||
# Get theme objects.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
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"
|
||||
)
|
||||
if self.data.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
# Update name
|
||||
if self.repository_manifest.filename is not None:
|
||||
self.information.file_name = self.repository_manifest.filename
|
||||
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}"
|
||||
)
|
||||
find_file_name(self)
|
||||
self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"
|
||||
|
9
config/custom_components/hacs/sensor.py
Executable file → Normal file
9
config/custom_components/hacs/sensor.py
Executable file → Normal file
@ -48,7 +48,7 @@ class HACSSensor(HACSDevice):
|
||||
for repository in hacs.repositories:
|
||||
if (
|
||||
repository.pending_upgrade
|
||||
and repository.category in hacs.common.categories
|
||||
and repository.data.category in hacs.common.categories
|
||||
):
|
||||
self.repositories.append(repository)
|
||||
self._state = len(self.repositories)
|
||||
@ -87,13 +87,10 @@ class HACSSensor(HACSDevice):
|
||||
for repository in self.repositories:
|
||||
data.append(
|
||||
{
|
||||
"name": repository.information.full_name,
|
||||
"name": repository.data.full_name,
|
||||
"display_name": repository.display_name,
|
||||
"installed version": repository.display_installed_version,
|
||||
"available version": repository.display_available_version,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"repositories": data,
|
||||
"attribution": "It is expected to see [object Object] here, for more info see https://hacs.xyz/docs/basic/sensor",
|
||||
}
|
||||
return {"repositories": data}
|
||||
|
0
config/custom_components/hacs/services.yaml
Executable file → Normal file
0
config/custom_components/hacs/services.yaml
Executable file → Normal file
59
config/custom_components/hacs/setup.py
Executable file → Normal file
59
config/custom_components/hacs/setup.py
Executable file → Normal file
@ -1,51 +1,57 @@
|
||||
"""Setup functions for HACS."""
|
||||
# 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."""
|
||||
from .const import VERSION
|
||||
from aiogithubapi import (
|
||||
AIOGitHubAuthentication,
|
||||
AIOGitHubException,
|
||||
AIOGitHubRatelimit,
|
||||
)
|
||||
hacs = get_hacs()
|
||||
|
||||
try:
|
||||
repository = hacs().get_by_name("hacs/integration")
|
||||
repository = hacs.get_by_name("hacs/integration")
|
||||
if repository is None:
|
||||
await hacs().register_repository("hacs/integration", "integration")
|
||||
repository = hacs().get_by_name("hacs/integration")
|
||||
await register_repository("hacs/integration", "integration")
|
||||
repository = hacs.get_by_name("hacs/integration")
|
||||
if repository is None:
|
||||
raise AIOGitHubException("Unknown error")
|
||||
raise HacsException("Unknown error")
|
||||
repository.status.installed = True
|
||||
repository.versions.installed = VERSION
|
||||
repository.status.new = False
|
||||
hacs.repo = repository.repository_object
|
||||
hacs.data_repo = await hacs().github.get_repo("hacs/default")
|
||||
except (
|
||||
AIOGitHubException,
|
||||
AIOGitHubRatelimit,
|
||||
AIOGitHubAuthentication,
|
||||
) as exception:
|
||||
hacs.logger.critical(f"[{exception}] - Could not load HACS!")
|
||||
hacs.data_repo = await get_repository(
|
||||
hacs.session, hacs.configuration.token, "hacs/default"
|
||||
)
|
||||
except HacsException as exception:
|
||||
if "403" in f"{exception}":
|
||||
hacs.logger.critical("GitHub API is ratelimited, or the token is wrong.")
|
||||
else:
|
||||
hacs.logger.critical(f"[{exception}] - Could not load HACS!")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def setup_extra_stores(hacs):
|
||||
def setup_extra_stores():
|
||||
"""Set up extra stores in HACS if enabled in Home Assistant."""
|
||||
hacs = get_hacs()
|
||||
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:
|
||||
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."""
|
||||
from .const import DOMAIN
|
||||
from homeassistant.helpers import discovery
|
||||
hacs = get_hacs()
|
||||
|
||||
try:
|
||||
if hacs.configuration.config_type == "yaml":
|
||||
@ -64,11 +70,12 @@ def add_sensor(hacs):
|
||||
pass
|
||||
|
||||
|
||||
async def setup_frontend(hacs):
|
||||
async def setup_frontend():
|
||||
"""Configure the HACS frontend elements."""
|
||||
from .http import HacsFrontend, HacsPluginViewLegacy
|
||||
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.frontend.version_running = FE_VERSION
|
||||
|
0
config/custom_components/hacs/store.py
Executable file → Normal file
0
config/custom_components/hacs/store.py
Executable file → Normal file
126
config/custom_components/hacs/ws_api_handlers.py
Executable file → Normal file
126
config/custom_components/hacs/ws_api_handlers.py
Executable file → Normal file
@ -6,10 +6,12 @@ import voluptuous as vol
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from homeassistant.components import websocket_api
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .hacsbase import Hacs
|
||||
from .hacsbase.exceptions import HacsException
|
||||
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):
|
||||
"""Set up WS API handlers."""
|
||||
@ -34,74 +36,76 @@ async def setup_ws_api(hass):
|
||||
)
|
||||
async def hacs_settings(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
hacs = get_hacs()
|
||||
action = msg["action"]
|
||||
Hacs().logger.debug(f"WS action '{action}'")
|
||||
hacs.logger.debug(f"WS action '{action}'")
|
||||
|
||||
if action == "set_fe_grid":
|
||||
Hacs().configuration.frontend_mode = "Grid"
|
||||
hacs.configuration.frontend_mode = "Grid"
|
||||
|
||||
elif action == "onboarding_done":
|
||||
Hacs().configuration.onboarding_done = True
|
||||
hacs.configuration.onboarding_done = True
|
||||
|
||||
elif action == "set_fe_table":
|
||||
Hacs().configuration.frontend_mode = "Table"
|
||||
hacs.configuration.frontend_mode = "Table"
|
||||
|
||||
elif action == "set_fe_compact_true":
|
||||
Hacs().configuration.frontend_compact = False
|
||||
hacs.configuration.frontend_compact = False
|
||||
|
||||
elif action == "set_fe_compact_false":
|
||||
Hacs().configuration.frontend_compact = True
|
||||
hacs.configuration.frontend_compact = True
|
||||
|
||||
elif action == "reload_data":
|
||||
Hacs().system.status.reloading_data = True
|
||||
hacs.system.status.reloading_data = True
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
await Hacs().recuring_tasks_all()
|
||||
Hacs().system.status.reloading_data = False
|
||||
await hacs.recuring_tasks_all()
|
||||
hacs.system.status.reloading_data = False
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
|
||||
elif action == "upgrade_all":
|
||||
Hacs().system.status.upgrading_all = True
|
||||
Hacs().system.status.background_task = True
|
||||
hacs.system.status.upgrading_all = True
|
||||
hacs.system.status.background_task = True
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
for repository in Hacs().repositories:
|
||||
for repository in hacs.repositories:
|
||||
if repository.pending_upgrade:
|
||||
repository.status.selected_tag = None
|
||||
await repository.install()
|
||||
Hacs().system.status.upgrading_all = False
|
||||
Hacs().system.status.background_task = False
|
||||
hacs.system.status.upgrading_all = False
|
||||
hacs.system.status.background_task = False
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
hass.bus.async_fire("hacs/repository", {})
|
||||
|
||||
elif action == "clear_new":
|
||||
for repo in Hacs().repositories:
|
||||
if msg.get("category") == repo.information.category:
|
||||
for repo in hacs.repositories:
|
||||
if msg.get("category") == repo.data.category:
|
||||
if repo.status.new:
|
||||
Hacs().logger.debug(
|
||||
f"Clearing new flag from '{repo.information.full_name}'"
|
||||
hacs.logger.debug(
|
||||
f"Clearing new flag from '{repo.data.full_name}'"
|
||||
)
|
||||
repo.status.new = False
|
||||
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", {})
|
||||
await Hacs().data.async_write()
|
||||
await hacs.data.async_write()
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({vol.Required("type"): "hacs/config"})
|
||||
async def hacs_config(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
config = Hacs().configuration
|
||||
hacs = get_hacs()
|
||||
config = hacs.configuration
|
||||
|
||||
content = {}
|
||||
content["frontend_mode"] = config.frontend_mode
|
||||
content["frontend_compact"] = config.frontend_compact
|
||||
content["onboarding_done"] = config.onboarding_done
|
||||
content["version"] = Hacs().version
|
||||
content["version"] = hacs.version
|
||||
content["dev"] = config.dev
|
||||
content["debug"] = config.debug
|
||||
content["country"] = config.country
|
||||
content["experimental"] = config.experimental
|
||||
content["categories"] = Hacs().common.categories
|
||||
content["categories"] = hacs.common.categories
|
||||
|
||||
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"})
|
||||
async def hacs_status(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
hacs = get_hacs()
|
||||
content = {
|
||||
"startup": Hacs().system.status.startup,
|
||||
"background_task": Hacs().system.status.background_task,
|
||||
"lovelace_mode": Hacs().system.lovelace_mode,
|
||||
"reloading_data": Hacs().system.status.reloading_data,
|
||||
"upgrading_all": Hacs().system.status.upgrading_all,
|
||||
"disabled": Hacs().system.disabled,
|
||||
"startup": hacs.system.status.startup,
|
||||
"background_task": hacs.system.status.background_task,
|
||||
"lovelace_mode": hacs.system.lovelace_mode,
|
||||
"reloading_data": hacs.system.status.reloading_data,
|
||||
"upgrading_all": hacs.system.status.upgrading_all,
|
||||
"disabled": hacs.system.disabled,
|
||||
}
|
||||
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"})
|
||||
async def hacs_repositories(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
repositories = Hacs().repositories
|
||||
hacs = get_hacs()
|
||||
repositories = hacs.repositories
|
||||
content = []
|
||||
for repo in repositories:
|
||||
if repo.information.category in Hacs().common.categories:
|
||||
if repo.data.category in hacs.common.categories:
|
||||
data = {
|
||||
"additional_info": repo.information.additional_info,
|
||||
"authors": repo.information.authors,
|
||||
"authors": repo.data.authors,
|
||||
"available_version": repo.display_available_version,
|
||||
"beta": repo.status.show_beta,
|
||||
"can_install": repo.can_install,
|
||||
"category": repo.information.category,
|
||||
"country": repo.repository_manifest.country,
|
||||
"category": repo.data.category,
|
||||
"country": repo.data.country,
|
||||
"config_flow": repo.config_flow,
|
||||
"custom": repo.custom,
|
||||
"default_branch": repo.information.default_branch,
|
||||
"description": repo.information.description,
|
||||
"domain": repo.manifest.get("domain"),
|
||||
"downloads": repo.releases.last_release_object_downloads,
|
||||
"file_name": repo.information.file_name,
|
||||
"default_branch": repo.data.default_branch,
|
||||
"description": repo.data.description,
|
||||
"domain": repo.integration_manifest.get("domain"),
|
||||
"downloads": repo.releases.downloads,
|
||||
"file_name": repo.data.file_name,
|
||||
"first_install": repo.status.first_install,
|
||||
"full_name": repo.information.full_name,
|
||||
"full_name": repo.data.full_name,
|
||||
"hide": repo.status.hide,
|
||||
"hide_default_branch": repo.repository_manifest.hide_default_branch,
|
||||
"homeassistant": repo.repository_manifest.homeassistant,
|
||||
"hide_default_branch": repo.data.hide_default_branch,
|
||||
"homeassistant": repo.data.homeassistant,
|
||||
"id": repo.information.uid,
|
||||
"info": repo.information.info,
|
||||
"installed_version": repo.display_installed_version,
|
||||
@ -162,11 +168,11 @@ async def hacs_repositories(hass, connection, msg):
|
||||
"pending_upgrade": repo.pending_upgrade,
|
||||
"releases": repo.releases.published_tags,
|
||||
"selected_tag": repo.status.selected_tag,
|
||||
"stars": repo.information.stars,
|
||||
"stars": repo.data.stargazers_count,
|
||||
"state": repo.state,
|
||||
"status_description": repo.display_status_description,
|
||||
"status": repo.display_status,
|
||||
"topics": repo.information.topics,
|
||||
"topics": repo.data.topics,
|
||||
"updated_info": repo.status.updated_info,
|
||||
"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):
|
||||
"""Handle get media player cover command."""
|
||||
hacs = get_hacs()
|
||||
try:
|
||||
repo_id = msg.get("repository")
|
||||
action = msg.get("action")
|
||||
@ -193,8 +200,8 @@ async def hacs_repository(hass, connection, msg):
|
||||
if repo_id is None or action is None:
|
||||
return
|
||||
|
||||
repository = Hacs().get_by_id(repo_id)
|
||||
Hacs().logger.debug(f"Running {action} for {repository.information.full_name}")
|
||||
repository = hacs.get_by_id(repo_id)
|
||||
hacs.logger.debug(f"Running {action} for {repository.data.full_name}")
|
||||
|
||||
if action == "update":
|
||||
await repository.update_repository()
|
||||
@ -230,17 +237,17 @@ async def hacs_repository(hass, connection, msg):
|
||||
repository.remove()
|
||||
|
||||
elif action == "set_version":
|
||||
if msg["version"] == repository.information.default_branch:
|
||||
if msg["version"] == repository.data.default_branch:
|
||||
repository.status.selected_tag = None
|
||||
else:
|
||||
repository.status.selected_tag = msg["version"]
|
||||
await repository.update_repository()
|
||||
|
||||
else:
|
||||
Hacs().logger.error(f"WS action '{action}' is not valid")
|
||||
hacs.logger.error(f"WS action '{action}' is not valid")
|
||||
|
||||
repository.state = None
|
||||
await Hacs().data.async_write()
|
||||
await hacs.data.async_write()
|
||||
except AIOGitHubException as exception:
|
||||
hass.bus.async_fire("hacs/error", {"message": str(exception)})
|
||||
except AttributeError as exception:
|
||||
@ -262,6 +269,7 @@ async def hacs_repository(hass, connection, msg):
|
||||
)
|
||||
async def hacs_repository_data(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
hacs = get_hacs()
|
||||
repo_id = msg.get("repository")
|
||||
action = msg.get("action")
|
||||
data = msg.get("data")
|
||||
@ -273,12 +281,12 @@ async def hacs_repository_data(hass, connection, msg):
|
||||
if "github." in repo_id:
|
||||
repo_id = repo_id.split("github.com/")[1]
|
||||
|
||||
if repo_id in Hacs().common.skip:
|
||||
Hacs().common.skip.remove(repo_id)
|
||||
if repo_id in hacs.common.skip:
|
||||
hacs.common.skip.remove(repo_id)
|
||||
|
||||
if not Hacs().get_by_name(repo_id):
|
||||
if not hacs.get_by_name(repo_id):
|
||||
try:
|
||||
registration = await Hacs().register_repository(repo_id, data.lower())
|
||||
registration = await register_repository(repo_id, data.lower())
|
||||
if registration is not None:
|
||||
raise HacsException(registration)
|
||||
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:
|
||||
repository = Hacs().get_by_id(repo_id)
|
||||
repository = hacs.get_by_id(repo_id)
|
||||
|
||||
if repository is None:
|
||||
hass.bus.async_fire("hacs/repository", {})
|
||||
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":
|
||||
repository.state = data
|
||||
@ -322,9 +330,9 @@ async def hacs_repository_data(hass, connection, msg):
|
||||
|
||||
else:
|
||||
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
|
||||
|
@ -11,13 +11,15 @@ cover:
|
||||
covers:
|
||||
large_garage:
|
||||
device: !secret large_garage_id
|
||||
username: !secret garadget_username
|
||||
password: !secret garadget_password
|
||||
#username: !secret garadget_username
|
||||
#password: !secret garadget_password
|
||||
access_token: !secret garadget_access_token
|
||||
name: large_garage
|
||||
small_garage:
|
||||
device: !secret small_garage_id
|
||||
username: !secret garadget_username
|
||||
password: !secret garadget_password
|
||||
#username: !secret garadget_username
|
||||
#password: !secret garadget_password
|
||||
access_token: !secret garadget_access_token
|
||||
name: small_garage
|
||||
|
||||
sensor:
|
||||
|
@ -6,17 +6,11 @@
|
||||
###################################
|
||||
|
||||
homeassistant:
|
||||
customize_glob:
|
||||
"sensor.skybell_*":
|
||||
icon: mdi:camera-front
|
||||
|
||||
hidden: False
|
||||
homebridge_hidden: True
|
||||
|
||||
group:
|
||||
skybell:
|
||||
name: Skybell HD Front Door
|
||||
|
||||
|
||||
entities:
|
||||
- binary_sensor.skybell_front_door_button
|
||||
- binary_sensor.skybell_front_door_motion
|
||||
@ -63,7 +57,7 @@ switch:
|
||||
## Doorbell Press
|
||||
automation:
|
||||
- alias: 'Log SkyBell Pressed Activity'
|
||||
|
||||
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
@ -87,7 +81,7 @@ automation:
|
||||
|
||||
# Motion Sensing
|
||||
- alias: 'Log SkyBell Motion detection'
|
||||
|
||||
|
||||
trigger:
|
||||
- platform: event
|
||||
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.
|
||||
|
||||
- alias: Automated Mismatch WatchDog!
|
||||
|
||||
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Loading…
x
Reference in New Issue
Block a user