Updated HACS and also fixed Garadget #727

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

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

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

@ -13,6 +13,8 @@
"integration": "Integration",
"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
View 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
View 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
View 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
View 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
View File

7
config/custom_components/hacs/.translations/it.json Executable file → Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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",

View 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
View File

@ -4,6 +4,7 @@ Custom element manager for community created elements.
For more details about this integration, please refer to the documentation at
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
View 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
View 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
View 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
View 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

View File

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

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

@ -9,13 +9,20 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit
from 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
View 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")

View 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
View File

82
config/custom_components/hacs/hacsbase/data.py Executable file → Normal file
View 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
View 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
View 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
View File

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

@ -7,15 +7,17 @@ import aiofiles
import async_timeout
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
View File

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

@ -16,9 +16,13 @@ class FileInformation:
def should_try_releases(repository):
"""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
View 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
View 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)

View File

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

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

@ -1,14 +1,17 @@
"""Install helper for repositories."""
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
View 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:

View File

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

View File

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

View File

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

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

@ -1,13 +1,14 @@
"""HACS http endpoints."""
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
View File

5
config/custom_components/hacs/manifest.json Executable file → Normal file
View 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
View 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
View 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
View 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
View File

View File

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

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

@ -1,26 +1,27 @@
"""Class for plugins in HACS."""
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

View 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)

View File

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

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

@ -1,29 +1,27 @@
"""Repository."""
# 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)

View File

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

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

@ -1,23 +1,22 @@
"""Class for themes in HACS."""
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
View 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
View File

59
config/custom_components/hacs/setup.py Executable file → Normal file
View 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
View File

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

@ -6,10 +6,12 @@ import voluptuous as vol
from aiogithubapi import AIOGitHubException
from 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

View File

@ -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:

View File

@ -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