From 38cc27cf0b5ea673713cf86ffbd316297a3434d1 Mon Sep 17 00:00:00 2001 From: eifinger Date: Tue, 21 Sep 2021 17:40:07 +0200 Subject: [PATCH] chore: update deps --- .../alexa_media/.translations/fr.json | 2 +- .../alexa_media/.translations/it.json | 2 +- .../alexa_media/.translations/pt_PT.json | 2 +- custom_components/alexa_media/__init__.py | 16 +- custom_components/alexa_media/alexa_entity.py | 6 +- custom_components/alexa_media/const.py | 2 +- custom_components/alexa_media/light.py | 275 +++++----- custom_components/alexa_media/manifest.json | 2 +- custom_components/alexa_media/sensor.py | 12 +- .../alexa_media/translations/fr.json | 2 +- .../alexa_media/translations/it.json | 2 +- .../alexa_media/translations/pt_PT.json | 2 +- custom_components/dwd_weather/connector.py | 4 +- custom_components/dwd_weather/manifest.json | 4 +- custom_components/dwd_weather/sensor.py | 2 +- custom_components/hacs/__init__.py | 52 +- .../api/acknowledge_critical_repository.py | 2 +- .../hacs/api/check_local_path.py | 2 +- .../hacs/api/get_critical_repositories.py | 2 +- custom_components/hacs/api/hacs_config.py | 2 +- custom_components/hacs/api/hacs_removed.py | 2 +- .../hacs/api/hacs_repositories.py | 7 +- custom_components/hacs/api/hacs_repository.py | 6 +- .../hacs/api/hacs_repository_data.py | 8 +- custom_components/hacs/api/hacs_settings.py | 4 +- custom_components/hacs/api/hacs_status.py | 4 +- custom_components/hacs/base.py | 281 +++++++--- custom_components/hacs/config_flow.py | 65 +-- custom_components/hacs/const.py | 27 +- custom_components/hacs/enums.py | 9 + .../hacs/{helpers/classes => }/exceptions.py | 6 +- .../hacs/hacsbase/configuration.py | 77 --- custom_components/hacs/hacsbase/data.py | 178 +++--- custom_components/hacs/hacsbase/hacs.py | 159 +++--- .../hacs/helpers/classes/manifest.py | 6 +- .../hacs/helpers/classes/repository.py | 59 +- .../hacs/helpers/classes/repositorydata.py | 111 ++-- .../hacs/helpers/functions/constrains.py | 43 -- .../hacs/helpers/functions/download.py | 84 ++- .../hacs/helpers/functions/filters.py | 4 +- .../functions/get_list_from_default.py | 28 +- .../hacs/helpers/functions/information.py | 27 +- .../helpers/functions/is_safe_to_remove.py | 16 +- .../hacs/helpers/functions/misc.py | 13 +- .../helpers/functions/register_repository.py | 34 +- .../functions/remaining_github_calls.py | 32 -- .../hacs/helpers/functions/save.py | 6 +- .../hacs/helpers/functions/store.py | 61 ++- .../hacs/helpers/functions/template.py | 2 +- .../helpers/functions/validate_repository.py | 30 +- .../hacs/helpers/methods/installation.py | 14 +- .../hacs/helpers/methods/registration.py | 4 +- .../helpers/properties/can_be_installed.py | 2 +- custom_components/hacs/manifest.json | 6 +- custom_components/hacs/mixin.py | 24 + custom_components/hacs/models/__init__.py | 1 - custom_components/hacs/models/core.py | 15 - custom_components/hacs/models/frontend.py | 10 - custom_components/hacs/models/system.py | 18 - custom_components/hacs/operational/backup.py | 10 +- custom_components/hacs/operational/factory.py | 4 +- custom_components/hacs/operational/reload.py | 10 - custom_components/hacs/operational/remove.py | 28 - custom_components/hacs/operational/runtime.py | 1 - custom_components/hacs/operational/setup.py | 220 +++----- .../operational/setup_actions/__init__.py | 0 .../operational/setup_actions/categories.py | 42 -- .../setup_actions/clear_storage.py | 24 - .../operational/setup_actions/frontend.py | 70 --- .../setup_actions/load_hacs_repository.py | 38 -- .../hacs/operational/setup_actions/sensor.py | 25 - .../setup_actions/websocket_api.py | 36 -- .../hacs/repositories/__init__.py | 24 +- .../hacs/repositories/appdaemon.py | 8 +- .../hacs/repositories/integration.py | 5 +- .../hacs/repositories/netdaemon.py | 17 +- custom_components/hacs/repositories/plugin.py | 9 +- .../hacs/repositories/python_script.py | 13 +- custom_components/hacs/repositories/theme.py | 5 +- custom_components/hacs/sensor.py | 23 +- custom_components/hacs/share.py | 4 +- custom_components/hacs/system_health.py | 33 +- custom_components/hacs/tasks/__init__.py | 1 + .../hacs/tasks/activate_categories.py | 35 ++ custom_components/hacs/tasks/base.py | 58 ++ .../hacs/tasks/check_constrains.py | 43 ++ .../hacs/tasks/clear_old_storage.py | 28 + custom_components/hacs/tasks/hello_world.py | 23 + .../hacs/tasks/load_hacs_repository.py | 40 ++ custom_components/hacs/tasks/manager.py | 75 +++ custom_components/hacs/tasks/restore_data.py | 23 + .../hacs/tasks/setup_frontend.py | 87 +++ custom_components/hacs/tasks/setup_sensor.py | 31 ++ .../hacs/tasks/setup_websocket_api.py | 42 ++ .../hacs/tasks/store_hacs_data.py | 22 + custom_components/hacs/tasks/verify_api.py | 23 + custom_components/hacs/translations/en.json | 4 + custom_components/hacs/utils/__init__.py | 1 + custom_components/hacs/utils/decode.py | 7 + .../{helpers/functions => utils}/logger.py | 2 +- custom_components/hacs/utils/path.py | 21 + custom_components/hacs/utils/version.py | 15 + .../hacs/webresponses/frontend.py | 6 +- custom_components/here_weather/__init__.py | 111 ++++ custom_components/here_weather/config_flow.py | 82 +++ custom_components/here_weather/const.py | 513 ++++++++++++++++++ custom_components/here_weather/manifest.json | 16 + custom_components/here_weather/sensor.py | 116 ++++ .../here_weather/translations/de.json | 19 + .../here_weather/translations/en.json | 19 + custom_components/here_weather/utils.py | 62 +++ custom_components/here_weather/weather.py | 296 ++++++++++ custom_components/ssh/manifest.json | 3 +- .../lovelace-auto-entities/auto-entities.js | 50 +- .../auto-entities.js.gz | Bin 12990 -> 13338 bytes .../rollup.config.js.gz | Bin 327 -> 327 bytes .../valetudo-map-card.js | 6 +- .../valetudo-map-card.js.gz | Bin 18223 -> 18260 bytes .../numberbox-card/numberbox-card.js | 36 +- .../numberbox-card/numberbox-card.js.gz | Bin 3316 -> 3417 bytes 120 files changed, 2968 insertions(+), 1475 deletions(-) rename custom_components/hacs/{helpers/classes => }/exceptions.py (71%) delete mode 100644 custom_components/hacs/hacsbase/configuration.py delete mode 100644 custom_components/hacs/helpers/functions/constrains.py delete mode 100644 custom_components/hacs/helpers/functions/remaining_github_calls.py create mode 100644 custom_components/hacs/mixin.py delete mode 100644 custom_components/hacs/models/__init__.py delete mode 100644 custom_components/hacs/models/core.py delete mode 100644 custom_components/hacs/models/frontend.py delete mode 100644 custom_components/hacs/models/system.py delete mode 100644 custom_components/hacs/operational/reload.py delete mode 100644 custom_components/hacs/operational/remove.py delete mode 100644 custom_components/hacs/operational/runtime.py delete mode 100644 custom_components/hacs/operational/setup_actions/__init__.py delete mode 100644 custom_components/hacs/operational/setup_actions/categories.py delete mode 100644 custom_components/hacs/operational/setup_actions/clear_storage.py delete mode 100644 custom_components/hacs/operational/setup_actions/frontend.py delete mode 100644 custom_components/hacs/operational/setup_actions/load_hacs_repository.py delete mode 100644 custom_components/hacs/operational/setup_actions/sensor.py delete mode 100644 custom_components/hacs/operational/setup_actions/websocket_api.py create mode 100644 custom_components/hacs/tasks/__init__.py create mode 100644 custom_components/hacs/tasks/activate_categories.py create mode 100644 custom_components/hacs/tasks/base.py create mode 100644 custom_components/hacs/tasks/check_constrains.py create mode 100644 custom_components/hacs/tasks/clear_old_storage.py create mode 100644 custom_components/hacs/tasks/hello_world.py create mode 100644 custom_components/hacs/tasks/load_hacs_repository.py create mode 100644 custom_components/hacs/tasks/manager.py create mode 100644 custom_components/hacs/tasks/restore_data.py create mode 100644 custom_components/hacs/tasks/setup_frontend.py create mode 100644 custom_components/hacs/tasks/setup_sensor.py create mode 100644 custom_components/hacs/tasks/setup_websocket_api.py create mode 100644 custom_components/hacs/tasks/store_hacs_data.py create mode 100644 custom_components/hacs/tasks/verify_api.py create mode 100644 custom_components/hacs/utils/__init__.py create mode 100644 custom_components/hacs/utils/decode.py rename custom_components/hacs/{helpers/functions => utils}/logger.py (92%) create mode 100644 custom_components/hacs/utils/path.py create mode 100644 custom_components/hacs/utils/version.py create mode 100644 custom_components/here_weather/__init__.py create mode 100644 custom_components/here_weather/config_flow.py create mode 100644 custom_components/here_weather/const.py create mode 100644 custom_components/here_weather/manifest.json create mode 100644 custom_components/here_weather/sensor.py create mode 100644 custom_components/here_weather/translations/de.json create mode 100644 custom_components/here_weather/translations/en.json create mode 100644 custom_components/here_weather/utils.py create mode 100644 custom_components/here_weather/weather.py diff --git a/custom_components/alexa_media/.translations/fr.json b/custom_components/alexa_media/.translations/fr.json index 75932fee..7bc3c144 100644 --- a/custom_components/alexa_media/.translations/fr.json +++ b/custom_components/alexa_media/.translations/fr.json @@ -8,7 +8,7 @@ "error": { "2fa_key_invalid": "Clé 2FA intégrée non valide", "connection_error": "Erreur de connexion; vérifier le réseau et réessayer", - "hass_url_invalid": "Impossible de se connecter à l'URL de Home Assistant. Veuillez vérifier l'URL interne sous Configuration - > Général", + "hass_url_invalid": "Impossible de se connecter à l'URL de Home Assistant. Veuillez vérifier l'URL externe sous Configuration - > Général", "identifier_exists": "Email pour l'URL Alexa déjà enregistré", "invalid_credentials": "Informations d'identification invalides", "unknown_error": "Erreur inconnue, veuillez signaler les informations du journal" diff --git a/custom_components/alexa_media/.translations/it.json b/custom_components/alexa_media/.translations/it.json index de45a5c9..f7a88a20 100644 --- a/custom_components/alexa_media/.translations/it.json +++ b/custom_components/alexa_media/.translations/it.json @@ -8,7 +8,7 @@ "error": { "2fa_key_invalid": "Invalid Built-In 2FA key", "connection_error": "Errore durante la connessione; controlla la rete e riprova", - "hass_url_invalid": "Impossibile collegarsi ad Home Assistant. Controllare l'URL interno nel menu Configurazione -> Generale", + "hass_url_invalid": "Impossibile collegarsi ad Home Assistant. Controllare l'URL esterno nel menu Configurazione -> Generale", "identifier_exists": "L'Email per l'URL di Alexa è già stata registrata", "invalid_credentials": "Credenziali non valide", "unknown_error": "Errore sconosciuto, si prega di abilitare il debug avanzato e riportare i log informativi" diff --git a/custom_components/alexa_media/.translations/pt_PT.json b/custom_components/alexa_media/.translations/pt_PT.json index 4edcbca9..1197e578 100644 --- a/custom_components/alexa_media/.translations/pt_PT.json +++ b/custom_components/alexa_media/.translations/pt_PT.json @@ -8,7 +8,7 @@ "error": { "2fa_key_invalid": "Chave 2FA integrada inválida", "connection_error": "Erro ao conectar; verifique a rede e tente novamente", - "hass_url_invalid": "Não foi possível conectar ao URL do Home Assistant. Verifique o URL interno em Configuração - > Geral", + "hass_url_invalid": "Não foi possível conectar ao URL do Home Assistant. Verifique o URL externo em Configuração - > Geral", "identifier_exists": "E-mail para URL Alexa já registado", "invalid_credentials": "Credenciais inválidas", "unknown_error": "Erro desconhecido, por favor habilite depuração avançada e informações de log de relatório" diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 5bbb7f2f..80b0db0b 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -621,16 +621,20 @@ async def async_update_data() -> Optional[AlexaEntityData]: device_registry, config_entry.entry_id ): for (_, identifier) in device_entry.identifiers: - if ( - identifier - in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ + if identifier in hass.data[DATA_ALEXAMEDIA]["accounts"][email][ + "devices" + ]["media_player"].keys() or identifier in map( + lambda x: slugify(f"{x}_{email}"), + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ "media_player" - ].keys() + ].keys(), ): break else: device_registry.async_remove_device(device_entry.id) - _LOGGER.debug("Removing stale device %s", device_entry.name) + _LOGGER.debug( + "%s: Removing stale device %s", hide_email(email), device_entry.name + ) await login_obj.save_cookiefile() if login_obj.access_token: @@ -656,7 +660,7 @@ async def process_notifications(login_obj, raw_notifications=None): previous = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "notifications", {} ) - notifications = {"process_timestamp": datetime.utcnow()} + notifications = {"process_timestamp": dt.utcnow()} for notification in raw_notifications: n_dev_id = notification.get("deviceSerialNumber") if n_dev_id is None: diff --git a/custom_components/alexa_media/alexa_entity.py b/custom_components/alexa_media/alexa_entity.py index 780a811d..a028902c 100644 --- a/custom_components/alexa_media/alexa_entity.py +++ b/custom_components/alexa_media/alexa_entity.py @@ -264,9 +264,7 @@ def parse_color_from_coordinator( if value is not None: hue = value.get("hue", 0) saturation = value.get("saturation", 0) - brightness = parse_brightness_from_coordinator(coordinator, entity_id, since) - if brightness is not None: - return hue, saturation, brightness / 100 + return hue, saturation, 1 return None @@ -324,7 +322,7 @@ def is_cap_state_still_acceptable( if formatted_time_of_sample: try: time_of_sample = datetime.strptime( - formatted_time_of_sample, "%Y-%m-%dT%H:%M:%S.%fZ" + formatted_time_of_sample, "%Y-%m-%dT%H:%M:%S.%f%z" ) return time_of_sample >= since except ValueError: diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index b5088635..c812ed3f 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -8,7 +8,7 @@ """ from datetime import timedelta -__version__ = "3.10.4" +__version__ = "3.10.8" PROJECT_URL = "https://github.com/custom-components/alexa_media_player/" ISSUE_URL = f"{PROJECT_URL}issues" diff --git a/custom_components/alexa_media/light.py b/custom_components/alexa_media/light.py index 0c418b8f..369cf071 100644 --- a/custom_components/alexa_media/light.py +++ b/custom_components/alexa_media/light.py @@ -311,8 +311,8 @@ async def _set_state(self, power_on, brightness=None, mired=None, hs=None): self._requested_hs = None else: self._requested_hs = self.hs_color - self._requested_state_at = ( - datetime.datetime.utcnow() + self._requested_state_at = datetime.datetime.now( + datetime.timezone.utc ) # must be set last so that previous getters work properly self.async_write_ha_state() @@ -362,125 +362,150 @@ def alexa_brightness_to_ha(alexa: Optional[float]) -> Optional[float]: return (alexa / 100 * 255) if alexa is not None else None -# This is a fairly complete list of all the colors that Alexa will respond to. -# A couple weirder ones are skipped because the HA color utility don't know the RGB value -ALEXA_COLORS = [ - "crimson", - "dark_red", - "firebrick", - "orange_red", - "red", - "deep_pink", - "hot_pink", - "light_pink", - "maroon", - "medium_violet_red", - "pale_violet_red", - "pink", - "plum", - "tomato", - "chocolate", - "dark_orange", - "maroon", - "coral", - "light_coral", - "light_salmon", - "peru", - "salmon", - "sienna", - "gold", - "goldenrod", - "lime", - "olive", - "yellow", - "chartreuse", - "dark_green", - "dark_olive_green", - "dark_sea_green", - "forest_green", - "green", - "green_yellow", - "lawn_green", - "light_green", - "lime_green", - "medium_sea_green", - "medium_spring_green", - "olive_drab", - "pale_green", - "sea_green", - "spring_green", - "yellow_green", - "blue", - "cadet_blue", - "cyan", - "dark_blue", - "dark_cyan", - "dark_slate_blue", - "dark_turquoise", - "deep_sky_blue", - "dodger_blue", - "light_blue", - "light_sea_green", - "light_sky_blue", - "medium_blue", - "medium_turquoise", - "midnight_blue", - "navy_blue", - "pale_turquoise", - "powder_blue", - "royal_blue", - "sky_blue", - "slate_blue", - "steel_blue", - "teal", - "turquoise", - "blue_violet", - "dark_magenta", - "dark_orchid", - "dark_violet", - "fuchsia", - "indigo", - "lavender", - "magenta", - "medium_orchid", - "medium_purple", - "orchid", - "purple", - "rosy_brown", - "violet", - "alice_blue", - "antique_white", - "blanched_almond", - "cornsilk", - "dark_khaki", - "floral_white", - "gainsboro", - "ghost_white", - "honeydew", - "ivory", - "khaki", - "lavender_blush", - "lemon_chiffon", - "light_cyan", - "light_steel_blue", - "light_yellow", - "linen", - "mint_cream", - "misty_rose", - "moccasin", - "old_lace", - "pale_goldenrod", - "papaya_whip", - "peach_puff", - "seashell", - "silver", - "snow", - "tan", - "thistle", - "wheat", - "white", - "white_smoke", -] +# This is a fairly complete list of all the colors that Alexa will respond to and their associated RGB value. +ALEXA_COLORS = { + "alice_blue": (240, 248, 255), + "antique_white": (250, 235, 215), + "aqua": (0, 255, 255), + "aquamarine": (127, 255, 212), + "azure": (240, 255, 255), + "beige": (245, 245, 220), + "bisque": (255, 228, 196), + "black": (0, 0, 0), + "blanched_almond": (255, 235, 205), + "blue": (0, 0, 255), + "blue_violet": (138, 43, 226), + "brown": (165, 42, 42), + "burlywood": (222, 184, 135), + "cadet_blue": (95, 158, 160), + "chartreuse": (127, 255, 0), + "chocolate": (210, 105, 30), + "coral": (255, 127, 80), + "cornflower_blue": (100, 149, 237), + "cornsilk": (255, 248, 220), + "crimson": (220, 20, 60), + "cyan": (0, 255, 255), + "dark_blue": (0, 0, 139), + "dark_cyan": (0, 139, 139), + "dark_goldenrod": (184, 134, 11), + "dark_green": (0, 100, 0), + "dark_grey": (169, 169, 169), + "dark_khaki": (189, 183, 107), + "dark_magenta": (139, 0, 139), + "dark_olive_green": (85, 107, 47), + "dark_orange": (255, 140, 0), + "dark_orchid": (153, 50, 204), + "dark_red": (139, 0, 0), + "dark_salmon": (233, 150, 122), + "dark_sea_green": (143, 188, 143), + "dark_slate_blue": (72, 61, 139), + "dark_slate_grey": (47, 79, 79), + "dark_turquoise": (0, 206, 209), + "dark_violet": (148, 0, 211), + "deep_pink": (255, 20, 147), + "deep_sky_blue": (0, 191, 255), + "dim_grey": (105, 105, 105), + "dodger_blue": (30, 144, 255), + "firebrick": (178, 34, 34), + "floral_white": (255, 250, 240), + "forest_green": (34, 139, 34), + "fuchsia": (255, 0, 255), + "gainsboro": (220, 220, 220), + "ghost_white": (248, 248, 255), + "gold": (255, 215, 0), + "goldenrod": (218, 165, 32), + "green": (0, 128, 0), + "green_yellow": (173, 255, 47), + "grey": (128, 128, 128), + "honey_dew": (240, 255, 240), + "hot_pink": (255, 105, 180), + "indian_red": (205, 92, 92), + "indigo": (75, 0, 130), + "ivory": (255, 255, 240), + "khaki": (240, 230, 140), + "lavender": (230, 230, 250), + "lavender_blush": (255, 240, 245), + "lawn_green": (124, 252, 0), + "lemon_chiffon": (255, 250, 205), + "light_blue": (173, 216, 230), + "light_coral": (240, 128, 128), + "light_cyan": (224, 255, 255), + "light_goldenrod_yellow": (250, 250, 210), + "light_green": (144, 238, 144), + "light_grey": (211, 211, 211), + "light_pink": (255, 182, 193), + "light_salmon": (255, 160, 122), + "light_sea_green": (32, 178, 170), + "light_sky_blue": (135, 206, 250), + "light_slate_grey": (119, 136, 153), + "light_steel_blue": (176, 196, 222), + "light_yellow": (255, 255, 224), + "lime": (0, 255, 0), + "lime_green": (50, 205, 50), + "linen": (250, 240, 230), + "magenta": (255, 0, 255), + "maroon": (128, 0, 0), + "medium_aqua_marine": (102, 205, 170), + "medium_blue": (0, 0, 205), + "medium_orchid": (186, 85, 211), + "medium_purple": (147, 112, 219), + "medium_sea_green": (60, 179, 113), + "medium_slate_blue": (123, 104, 238), + "medium_spring_green": (0, 250, 154), + "medium_turquoise": (72, 209, 204), + "medium_violet_red": (199, 21, 133), + "midnight_blue": (25, 25, 112), + "mint_cream": (245, 255, 250), + "misty_rose": (255, 228, 225), + "moccasin": (255, 228, 181), + "navajo_white": (255, 222, 173), + "navy": (0, 0, 128), + "old_lace": (253, 245, 230), + "olive": (128, 128, 0), + "olive_drab": (107, 142, 35), + "orange": (255, 165, 0), + "orange_red": (255, 69, 0), + "orchid": (218, 112, 214), + "pale_goldenrod": (238, 232, 170), + "pale_green": (152, 251, 152), + "pale_turquoise": (175, 238, 238), + "pale_violet_red": (219, 112, 147), + "papaya_whip": (255, 239, 213), + "peach_puff": (255, 218, 185), + "peru": (205, 133, 63), + "pink": (255, 192, 203), + "plum": (221, 160, 221), + "powder_blue": (176, 224, 230), + "purple": (128, 0, 128), + "rebecca_purple": (102, 51, 153), + "red": (255, 0, 0), + "rosy_brown": (188, 143, 143), + "royal_blue": (65, 105, 225), + "saddle_brown": (139, 69, 19), + "salmon": (250, 128, 114), + "sandy_brown": (244, 164, 96), + "sea_green": (46, 139, 87), + "sea_shell": (255, 245, 238), + "sienna": (160, 82, 45), + "silver": (192, 192, 192), + "sky_blue": (135, 206, 235), + "slate_blue": (106, 90, 205), + "slate_grey": (112, 128, 144), + "snow": (255, 250, 250), + "spring_green": (0, 255, 127), + "steel_blue": (70, 130, 180), + "tan": (210, 180, 140), + "teal": (0, 128, 128), + "thistle": (216, 191, 216), + "tomato": (255, 99, 71), + "turquoise": (64, 224, 208), + "violet": (238, 130, 238), + "wheat": (245, 222, 179), + "white": (255, 255, 255), + "white_smoke": (245, 245, 245), + "yellow": (255, 255, 0), + "yellow_green": (154, 205, 50), +} def red_mean(color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> float: @@ -506,11 +531,11 @@ def rgb_to_alexa_color( rgb: Tuple[int, int, int] ) -> Tuple[Optional[Tuple[float, float]], Optional[Text]]: """Convert a given RGB value into the closest Alexa color.""" - name = min( - ALEXA_COLORS, - key=lambda color_name: red_mean(rgb, alexa_color_name_to_rgb(color_name)), + (name, alexa_rgb) = min( + ALEXA_COLORS.items(), + key=lambda alexa_color: red_mean(alexa_color[1], rgb), ) - red, green, blue = alexa_color_name_to_rgb(name) + red, green, blue = alexa_rgb return color_RGB_to_hs(red, green, blue), name diff --git a/custom_components/alexa_media/manifest.json b/custom_components/alexa_media/manifest.json index eaaddbbe..c506a1db 100644 --- a/custom_components/alexa_media/manifest.json +++ b/custom_components/alexa_media/manifest.json @@ -1,7 +1,7 @@ { "domain": "alexa_media", "name": "Alexa Media Player", - "version": "3.10.4", + "version": "3.10.8", "config_flow": true, "documentation": "https://github.com/custom-components/alexa_media_player/wiki", "issue_tracker": "https://github.com/custom-components/alexa_media_player/issues", diff --git a/custom_components/alexa_media/sensor.py b/custom_components/alexa_media/sensor.py index 0166adac..7030e251 100644 --- a/custom_components/alexa_media/sensor.py +++ b/custom_components/alexa_media/sensor.py @@ -370,7 +370,7 @@ def _update_recurring_alarm(self, value): while ( alarm_on and recurring_pattern - and RECURRING_PATTERN_ISO_SET[recurring_pattern] + and RECURRING_PATTERN_ISO_SET.get(recurring_pattern) and alarm.isoweekday not in RECURRING_PATTERN_ISO_SET[recurring_pattern] and alarm < dt.now() ): @@ -538,9 +538,7 @@ def device_state_attributes(self): attr = { "recurrence": self.recurrence, - "process_timestamp": dt.as_local( - datetime.datetime.fromtimestamp(self._timestamp.timestamp()) - ).isoformat(), + "process_timestamp": dt.as_local(self._timestamp).isoformat(), "prior_value": self._process_state(self._prior_value), "total_active": len(self._active), "total_all": len(self._all), @@ -586,10 +584,8 @@ def _process_state(self, value): return ( dt.as_local( super()._round_time( - datetime.datetime.fromtimestamp( - self._timestamp.timestamp() - + value[self._sensor_property] / 1000 - ) + self._timestamp + + datetime.timedelta(milliseconds=value[self._sensor_property]) ) ).isoformat() if value and self._timestamp diff --git a/custom_components/alexa_media/translations/fr.json b/custom_components/alexa_media/translations/fr.json index 75932fee..7bc3c144 100644 --- a/custom_components/alexa_media/translations/fr.json +++ b/custom_components/alexa_media/translations/fr.json @@ -8,7 +8,7 @@ "error": { "2fa_key_invalid": "Clé 2FA intégrée non valide", "connection_error": "Erreur de connexion; vérifier le réseau et réessayer", - "hass_url_invalid": "Impossible de se connecter à l'URL de Home Assistant. Veuillez vérifier l'URL interne sous Configuration - > Général", + "hass_url_invalid": "Impossible de se connecter à l'URL de Home Assistant. Veuillez vérifier l'URL externe sous Configuration - > Général", "identifier_exists": "Email pour l'URL Alexa déjà enregistré", "invalid_credentials": "Informations d'identification invalides", "unknown_error": "Erreur inconnue, veuillez signaler les informations du journal" diff --git a/custom_components/alexa_media/translations/it.json b/custom_components/alexa_media/translations/it.json index de45a5c9..f7a88a20 100644 --- a/custom_components/alexa_media/translations/it.json +++ b/custom_components/alexa_media/translations/it.json @@ -8,7 +8,7 @@ "error": { "2fa_key_invalid": "Invalid Built-In 2FA key", "connection_error": "Errore durante la connessione; controlla la rete e riprova", - "hass_url_invalid": "Impossibile collegarsi ad Home Assistant. Controllare l'URL interno nel menu Configurazione -> Generale", + "hass_url_invalid": "Impossibile collegarsi ad Home Assistant. Controllare l'URL esterno nel menu Configurazione -> Generale", "identifier_exists": "L'Email per l'URL di Alexa è già stata registrata", "invalid_credentials": "Credenziali non valide", "unknown_error": "Errore sconosciuto, si prega di abilitare il debug avanzato e riportare i log informativi" diff --git a/custom_components/alexa_media/translations/pt_PT.json b/custom_components/alexa_media/translations/pt_PT.json index 4edcbca9..1197e578 100644 --- a/custom_components/alexa_media/translations/pt_PT.json +++ b/custom_components/alexa_media/translations/pt_PT.json @@ -8,7 +8,7 @@ "error": { "2fa_key_invalid": "Chave 2FA integrada inválida", "connection_error": "Erro ao conectar; verifique a rede e tente novamente", - "hass_url_invalid": "Não foi possível conectar ao URL do Home Assistant. Verifique o URL interno em Configuração - > Geral", + "hass_url_invalid": "Não foi possível conectar ao URL do Home Assistant. Verifique o URL externo em Configuração - > Geral", "identifier_exists": "E-mail para URL Alexa já registado", "invalid_credentials": "Credenciais inválidas", "unknown_error": "Erro desconhecido, por favor habilite depuração avançada e informações de log de relatório" diff --git a/custom_components/dwd_weather/connector.py b/custom_components/dwd_weather/connector.py index c350e998..c98f4358 100644 --- a/custom_components/dwd_weather/connector.py +++ b/custom_components/dwd_weather/connector.py @@ -272,11 +272,11 @@ def get_hourly(self, data_type: WeatherDataType): elif data_type == WeatherDataType.PRESSURE: value = round(value / 100, 1) elif data_type == WeatherDataType.WIND_SPEED: - value = round(value, 1) + value = round(value * 3.6, 1) elif data_type == WeatherDataType.WIND_DIRECTION: value = round(value, 0) elif data_type == WeatherDataType.WIND_GUSTS: - value = round(value, 1) + value = round(value * 3.6, 1) elif data_type == WeatherDataType.PRECIPITATION: value = round(value, 1) elif data_type == WeatherDataType.PRECIPITATION_PROBABILITY: diff --git a/custom_components/dwd_weather/manifest.json b/custom_components/dwd_weather/manifest.json index ae201bdb..16d49775 100644 --- a/custom_components/dwd_weather/manifest.json +++ b/custom_components/dwd_weather/manifest.json @@ -1,6 +1,6 @@ { "domain": "dwd_weather", - "version": "1.2.11", + "version": "1.2.15", "name": "Deutscher Wetterdienst (DWD)", "documentation": "https://github.com/FL550/dwd_weather", "issue_tracker": "https://github.com/FL550/dwd_weather/issues", @@ -9,5 +9,5 @@ "codeowners": [ "@FL550" ], - "requirements": ["simple_dwd_weatherforecast==1.0.17","markdownify==0.6.5"] + "requirements": ["simple_dwd_weatherforecast==1.1.1","markdownify==0.6.5"] } diff --git a/custom_components/dwd_weather/sensor.py b/custom_components/dwd_weather/sensor.py index 475c10a4..0adf8882 100644 --- a/custom_components/dwd_weather/sensor.py +++ b/custom_components/dwd_weather/sensor.py @@ -99,7 +99,7 @@ "sun_irradiance": [ "Sun Irradiance", None, - "kJ/m^2", + "W/m^2", "mdi:weather-sunny-alert", False, ], diff --git a/custom_components/hacs/__init__.py b/custom_components/hacs/__init__.py index 5adcd488..db07e044 100644 --- a/custom_components/hacs/__init__.py +++ b/custom_components/hacs/__init__.py @@ -4,29 +4,65 @@ For more details about this integration, please refer to the documentation at https://hacs.xyz/ """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant import voluptuous as vol -from .const import DOMAIN +from .const import DOMAIN, PLATFORMS +from .enums import HacsDisabledReason from .helpers.functions.configuration_schema import hacs_config_combined -from .operational.setup import async_setup as hacs_yaml_setup -from .operational.setup import async_setup_entry as hacs_ui_setup -from .operational.remove import async_remove_entry as hacs_remove_entry +from .operational.setup import ( + async_setup as hacs_yaml_setup, + async_setup_entry as hacs_ui_setup, +) + +if TYPE_CHECKING: + from .base import HacsBase CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Set up this integration using yaml.""" return await hacs_yaml_setup(hass, config) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up this integration using UI.""" + config_entry.add_update_listener(async_reload_entry) return await hacs_ui_setup(hass, config_entry) -async def async_remove_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Handle removal of an entry.""" - return await hacs_remove_entry(hass, config_entry) + hacs: HacsBase = hass.data[DOMAIN] + + for task in hacs.recuring_tasks: + # Cancel all pending tasks + task() + + try: + if hass.data.get("frontend_panels", {}).get("hacs"): + hacs.log.info("Removing sidepanel") + hass.components.frontend.async_remove_panel("hacs") + except AttributeError: + pass + + unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + hacs.disable_hacs(HacsDisabledReason.REMOVED) + hass.data.pop(DOMAIN, None) + + return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Reload the HACS config entry.""" + await async_unload_entry(hass, config_entry) + await async_setup_entry(hass, config_entry) diff --git a/custom_components/hacs/api/acknowledge_critical_repository.py b/custom_components/hacs/api/acknowledge_critical_repository.py index 02235b21..f0d69bd9 100644 --- a/custom_components/hacs/api/acknowledge_critical_repository.py +++ b/custom_components/hacs/api/acknowledge_critical_repository.py @@ -1,7 +1,7 @@ """API Handler for acknowledge_critical_repository""" +from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv import voluptuous as vol -from homeassistant.components import websocket_api from custom_components.hacs.helpers.functions.store import ( async_load_from_store, diff --git a/custom_components/hacs/api/check_local_path.py b/custom_components/hacs/api/check_local_path.py index 308b81be..d45526bb 100644 --- a/custom_components/hacs/api/check_local_path.py +++ b/custom_components/hacs/api/check_local_path.py @@ -1,7 +1,7 @@ """API Handler for check_local_path""" +from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv import voluptuous as vol -from homeassistant.components import websocket_api from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist diff --git a/custom_components/hacs/api/get_critical_repositories.py b/custom_components/hacs/api/get_critical_repositories.py index 35f2c946..23fdcf4d 100644 --- a/custom_components/hacs/api/get_critical_repositories.py +++ b/custom_components/hacs/api/get_critical_repositories.py @@ -1,6 +1,6 @@ """API Handler for get_critical_repositories""" -import voluptuous as vol from homeassistant.components import websocket_api +import voluptuous as vol from custom_components.hacs.helpers.functions.store import async_load_from_store diff --git a/custom_components/hacs/api/hacs_config.py b/custom_components/hacs/api/hacs_config.py index 1760ee8f..35609a6d 100644 --- a/custom_components/hacs/api/hacs_config.py +++ b/custom_components/hacs/api/hacs_config.py @@ -1,6 +1,6 @@ """API Handler for hacs_config""" -import voluptuous as vol from homeassistant.components import websocket_api +import voluptuous as vol from custom_components.hacs.share import get_hacs diff --git a/custom_components/hacs/api/hacs_removed.py b/custom_components/hacs/api/hacs_removed.py index 28628e87..7ede24e9 100644 --- a/custom_components/hacs/api/hacs_removed.py +++ b/custom_components/hacs/api/hacs_removed.py @@ -1,6 +1,6 @@ """API Handler for hacs_removed""" -import voluptuous as vol from homeassistant.components import websocket_api +import voluptuous as vol from custom_components.hacs.share import list_removed_repositories diff --git a/custom_components/hacs/api/hacs_repositories.py b/custom_components/hacs/api/hacs_repositories.py index e0033427..d4d79f5d 100644 --- a/custom_components/hacs/api/hacs_repositories.py +++ b/custom_components/hacs/api/hacs_repositories.py @@ -1,6 +1,6 @@ """API Handler for hacs_repositories""" -import voluptuous as vol from homeassistant.components import websocket_api +import voluptuous as vol from custom_components.hacs.share import get_hacs @@ -13,7 +13,10 @@ async def hacs_repositories(_hass, connection, msg): repositories = hacs.repositories content = [] for repo in repositories: - if repo.data.category in hacs.common.categories: + if ( + repo.data.category in hacs.common.categories + and not repo.ignored_by_country_configuration + ): data = { "additional_info": repo.information.additional_info, "authors": repo.data.authors, diff --git a/custom_components/hacs/api/hacs_repository.py b/custom_components/hacs/api/hacs_repository.py index 6921db43..bfa00234 100644 --- a/custom_components/hacs/api/hacs_repository.py +++ b/custom_components/hacs/api/hacs_repository.py @@ -1,11 +1,11 @@ """API Handler for hacs_repository""" -import homeassistant.helpers.config_validation as cv -import voluptuous as vol from aiogithubapi import AIOGitHubAPIException from homeassistant.components import websocket_api +import homeassistant.helpers.config_validation as cv +import voluptuous as vol -from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.share import get_hacs +from custom_components.hacs.utils.logger import getLogger @websocket_api.async_response diff --git a/custom_components/hacs/api/hacs_repository_data.py b/custom_components/hacs/api/hacs_repository_data.py index 3052e72d..123d8dd2 100644 --- a/custom_components/hacs/api/hacs_repository_data.py +++ b/custom_components/hacs/api/hacs_repository_data.py @@ -1,18 +1,18 @@ """API Handler for hacs_repository_data""" import sys -import homeassistant.helpers.config_validation as cv -import voluptuous as vol from aiogithubapi import AIOGitHubAPIException from homeassistant.components import websocket_api +import homeassistant.helpers.config_validation as cv +import voluptuous as vol -from custom_components.hacs.helpers.classes.exceptions import HacsException -from custom_components.hacs.helpers.functions.logger import getLogger +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.functions.misc import extract_repository_from_url from custom_components.hacs.helpers.functions.register_repository import ( register_repository, ) from custom_components.hacs.share import get_hacs +from custom_components.hacs.utils.logger import getLogger _LOGGER = getLogger() diff --git a/custom_components/hacs/api/hacs_settings.py b/custom_components/hacs/api/hacs_settings.py index fe076348..7af3468c 100644 --- a/custom_components/hacs/api/hacs_settings.py +++ b/custom_components/hacs/api/hacs_settings.py @@ -1,10 +1,10 @@ """API Handler for hacs_settings""" +from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv import voluptuous as vol -from homeassistant.components import websocket_api -from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.share import get_hacs +from custom_components.hacs.utils.logger import getLogger _LOGGER = getLogger() diff --git a/custom_components/hacs/api/hacs_status.py b/custom_components/hacs/api/hacs_status.py index f840cc4b..15ce85cb 100644 --- a/custom_components/hacs/api/hacs_status.py +++ b/custom_components/hacs/api/hacs_status.py @@ -1,6 +1,6 @@ """API Handler for hacs_status""" -import voluptuous as vol from homeassistant.components import websocket_api +import voluptuous as vol from custom_components.hacs.share import get_hacs @@ -13,7 +13,7 @@ async def hacs_status(_hass, connection, msg): content = { "startup": hacs.status.startup, "background_task": hacs.status.background_task, - "lovelace_mode": hacs.system.lovelace_mode, + "lovelace_mode": hacs.core.lovelace_mode, "reloading_data": hacs.status.reloading_data, "upgrading_all": hacs.status.upgrading_all, "disabled": hacs.system.disabled, diff --git a/custom_components/hacs/base.py b/custom_components/hacs/base.py index 5df325d2..54d5759a 100644 --- a/custom_components/hacs/base.py +++ b/custom_components/hacs/base.py @@ -1,32 +1,119 @@ """Base HACS class.""" +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +import json import logging -from typing import List, Optional, TYPE_CHECKING +import math import pathlib - -import attr -from aiogithubapi.github import AIOGitHubAPI +from typing import TYPE_CHECKING, Any + +from aiogithubapi import ( + GitHub, + GitHubAPI, + GitHubAuthenticationException, + GitHubRatelimitException, +) from aiogithubapi.objects.repository import AIOGitHubAPIRepository +from aiohttp.client import ClientSession from homeassistant.core import HomeAssistant - -from .enums import HacsDisabledReason, HacsStage -from .helpers.functions.logger import getLogger -from .models.core import HacsCore -from .models.frontend import HacsFrontend -from .models.system import HacsSystem +from homeassistant.loader import Integration +from queueman.manager import QueueManager + +from .const import REPOSITORY_HACS_DEFAULT +from .enums import ( + ConfigurationType, + HacsCategory, + HacsDisabledReason, + HacsStage, + LovelaceMode, +) +from .exceptions import HacsException +from .utils.decode import decode_content +from .utils.logger import getLogger if TYPE_CHECKING: + from .hacsbase.data import HacsData from .helpers.classes.repository import HacsRepository - - + from .operational.factory import HacsTaskFactory + from .tasks.manager import HacsTaskManager + + +@dataclass +class HacsConfiguration: + """HacsConfiguration class.""" + + appdaemon_path: str = "appdaemon/apps/" + appdaemon: bool = False + config: dict[str, Any] = field(default_factory=dict) + config_entry: dict[str, str] = field(default_factory=dict) + config_type: ConfigurationType | None = None + country: str = "ALL" + debug: bool = False + dev: bool = False + experimental: bool = False + frontend_compact: bool = False + frontend_mode: str = "Grid" + frontend_repo_url: str = "" + frontend_repo: str = "" + netdaemon_path: str = "netdaemon/apps/" + netdaemon: bool = False + onboarding_done: bool = False + plugin_path: str = "www/community/" + python_script_path: str = "python_scripts/" + python_script: bool = False + release_limit: int = 5 + sidepanel_icon: str = "hacs:hacs" + sidepanel_title: str = "HACS" + theme_path: str = "themes/" + theme: bool = False + token: str = None + + def to_json(self) -> str: + """Return a json string.""" + return asdict(self) + + def update_from_dict(self, data: dict) -> None: + """Set attributes from dicts.""" + if not isinstance(data, dict): + raise HacsException("Configuration is not valid.") + + for key in data: + self.__setattr__(key, data[key]) + + +@dataclass +class HacsFrontend: + """HacsFrontend.""" + + version_running: str | None = None + version_available: str | None = None + version_expected: str | None = None + update_pending: bool = False + + +@dataclass +class HacsCore: + """HACS Core info.""" + + config_path: pathlib.Path | None = None + ha_version: str | None = None + lovelace_mode = LovelaceMode("yaml") + + +@dataclass class HacsCommon: """Common for HACS.""" - categories: List = [] - default: List = [] - installed: List = [] - skip: List = [] + categories: set[str] = field(default_factory=set) + default: list[str] = field(default_factory=list) + installed: list[str] = field(default_factory=list) + renamed_repositories: dict[str, str] = field(default_factory=dict) + archived_repositories: list[str] = field(default_factory=list) + skip: list[str] = field(default_factory=list) +@dataclass class HacsStatus: """HacsStatus.""" @@ -37,93 +124,111 @@ class HacsStatus: upgrading_all: bool = False -@attr.s -class HacsBaseAttributes: - """Base HACS class.""" +@dataclass +class HacsSystem: + """HACS System info.""" - _default: Optional[AIOGitHubAPIRepository] - _github: Optional[AIOGitHubAPI] - _hass: Optional[HomeAssistant] - _repository: Optional[AIOGitHubAPIRepository] - _stage: HacsStage = HacsStage.SETUP - _common: Optional[HacsCommon] - - core: HacsCore = attr.ib(HacsCore) - common: HacsCommon = attr.ib(HacsCommon) - status: HacsStatus = attr.ib(HacsStatus) - frontend: HacsFrontend = attr.ib(HacsFrontend) - log: logging.Logger = getLogger() - system: HacsSystem = attr.ib(HacsSystem) - repositories: List["HacsRepository"] = [] + disabled: bool = False + disabled_reason: str | None = None + running: bool = False + stage = HacsStage.SETUP + action: bool = False -@attr.s -class HacsBase(HacsBaseAttributes): +class HacsBase: """Base HACS class.""" - @property - def stage(self) -> HacsStage: - """Returns a HacsStage object.""" - return self._stage - - @stage.setter - def stage(self, value: HacsStage) -> None: - """Set the value for the stage property.""" - self._stage = value - - @property - def github(self) -> Optional[AIOGitHubAPI]: - """Returns a AIOGitHubAPI object.""" - return self._github - - @github.setter - def github(self, value: AIOGitHubAPI) -> None: - """Set the value for the github property.""" - self._github = value - - @property - def repository(self) -> Optional[AIOGitHubAPIRepository]: - """Returns a AIOGitHubAPIRepository object representing hacs/integration.""" - return self._repository - - @repository.setter - def repository(self, value: AIOGitHubAPIRepository) -> None: - """Set the value for the repository property.""" - self._repository = value - - @property - def default(self) -> Optional[AIOGitHubAPIRepository]: - """Returns a AIOGitHubAPIRepository object representing hacs/default.""" - return self._default - - @default.setter - def default(self, value: AIOGitHubAPIRepository) -> None: - """Set the value for the default property.""" - self._default = value - - @property - def hass(self) -> Optional[HomeAssistant]: - """Returns a HomeAssistant object.""" - return self._hass - - @hass.setter - def hass(self, value: HomeAssistant) -> None: - """Set the value for the default property.""" - self._hass = value + _repositories = [] + _repositories_by_full_name = {} + _repositories_by_id = {} + + common = HacsCommon() + configuration = HacsConfiguration() + core = HacsCore() + data: HacsData | None = None + factory: HacsTaskFactory | None = None + frontend = HacsFrontend() + github: GitHub | None = None + githubapi: GitHubAPI | None = None + hass: HomeAssistant | None = None + integration: Integration | None = None + log: logging.Logger = getLogger() + queue: QueueManager | None = None + recuring_tasks = [] + repositories: list[HacsRepository] = [] + repository: AIOGitHubAPIRepository | None = None + session: ClientSession | None = None + stage: HacsStage | None = None + status = HacsStatus() + system = HacsSystem() + tasks: HacsTaskManager | None = None + version: str | None = None @property def integration_dir(self) -> pathlib.Path: """Return the HACS integration dir.""" - return pathlib.Path(__file__).parent + return self.integration.file_path - def disable(self, reason: HacsDisabledReason) -> None: + async def async_set_stage(self, stage: HacsStage | None) -> None: + """Set HACS stage.""" + if stage and self.stage == stage: + return + + self.stage = stage + if stage is not None: + self.log.info("Stage changed: %s", self.stage) + self.hass.bus.async_fire("hacs/stage", {"stage": self.stage}) + await self.tasks.async_execute_runtume_tasks() + + def disable_hacs(self, reason: HacsDisabledReason) -> None: """Disable HACS.""" self.system.disabled = True self.system.disabled_reason = reason - self.log.error("HACS is disabled - %s", reason) + if reason != HacsDisabledReason.REMOVED: + self.log.error("HACS is disabled - %s", reason) - def enable(self) -> None: + def enable_hacs(self) -> None: """Enable HACS.""" self.system.disabled = False self.system.disabled_reason = None self.log.info("HACS is enabled") + + def enable_hacs_category(self, category: HacsCategory): + """Enable HACS category.""" + if category not in self.common.categories: + self.log.info("Enable category: %s", category) + self.common.categories.add(category) + + def disable_hacs_category(self, category: HacsCategory): + """Disable HACS category.""" + if category in self.common.categories: + self.log.info("Disabling category: %s", category) + self.common.categories.pop(category) + + async def async_can_update(self) -> int: + """Helper to calculate the number of repositories we can fetch data for.""" + try: + response = await self.githubapi.rate_limit() + if ((limit := response.data.resources.core.remaining or 0) - 1000) >= 15: + return math.floor((limit - 1000) / 15) + self.log.error( + "GitHub API ratelimited - %s remaining", response.data.resources.core.remaining + ) + self.disable_hacs(HacsDisabledReason.RATE_LIMIT) + except GitHubAuthenticationException as exception: + self.log.error("GitHub authentication failed - %s", exception) + self.disable_hacs(HacsDisabledReason.INVALID_TOKEN) + except GitHubRatelimitException as exception: + self.log.error("GitHub API ratelimited - %s", exception) + self.disable_hacs(HacsDisabledReason.RATE_LIMIT) + except BaseException as exception: # pylint: disable=broad-except + self.log.exception(exception) + + return 0 + + async def async_github_get_hacs_default_file(self, filename: str) -> dict[str, Any]: + """Get the content of a default file.""" + response = await self.githubapi.repos.contents.get( + repository=REPOSITORY_HACS_DEFAULT, path=filename + ) + return json.loads(decode_content(response.data.content)) diff --git a/custom_components/hacs/config_flow.py b/custom_components/hacs/config_flow.py index dc25d273..d4930777 100644 --- a/custom_components/hacs/config_flow.py +++ b/custom_components/hacs/config_flow.py @@ -1,6 +1,5 @@ """Adds config flow for HACS.""" -import voluptuous as vol -from aiogithubapi import AIOGitHubAPIException, GitHubDevice +from aiogithubapi import GitHubDeviceAPI, GitHubException from aiogithubapi.common.const import OAUTH_USER_LOGIN from awesomeversion import AwesomeVersion from homeassistant import config_entries @@ -8,20 +7,19 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from homeassistant.helpers.event import async_call_later +from homeassistant.loader import async_get_integration +import voluptuous as vol from custom_components.hacs.const import CLIENT_ID, DOMAIN, MINIMUM_HA_VERSION +from custom_components.hacs.enums import ConfigurationType from custom_components.hacs.helpers.functions.configuration_schema import ( + RELEASE_LIMIT, hacs_config_option_schema, ) -from custom_components.hacs.helpers.functions.logger import getLogger -from custom_components.hacs.share import get_hacs - -from .base import HacsBase +from custom_components.hacs.mixin import HacsMixin -_LOGGER = getLogger() - -class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class HacsFlowHandler(HacsMixin, config_entries.ConfigFlow, domain=DOMAIN): """Config flow for HACS.""" VERSION = 1 @@ -33,6 +31,7 @@ def __init__(self): self.device = None self.activation = None self._progress_task = None + self._login_device = None async def async_step_user(self, user_input): """Handle a flow initialized by the user.""" @@ -56,30 +55,38 @@ async def async_step_device(self, _user_input): """Handle device steps""" async def _wait_for_activation(_=None): - self.activation = await self.device.async_device_activation() + if self._login_device is None or self._login_device.expires_in is None: + async_call_later(self.hass, 1, _wait_for_activation) + return + + response = await self.device.activation(device_code=self._login_device.device_code) + self.activation = response.data self.hass.async_create_task( self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) ) if not self.activation: + integration = await async_get_integration(self.hass, DOMAIN) if not self.device: - self.device = GitHubDevice( - CLIENT_ID, + self.device = GitHubDeviceAPI( + client_id=CLIENT_ID, session=aiohttp_client.async_get_clientsession(self.hass), + **{"client_name": f"HACS/{integration.version}"}, ) async_call_later(self.hass, 1, _wait_for_activation) try: - device_data = await self.device.async_register_device() + response = await self.device.register() + self._login_device = response.data return self.async_show_progress( step_id="device", progress_action="wait_for_device", description_placeholders={ "url": OAUTH_USER_LOGIN, - "code": device_data.user_code, + "code": self._login_device.user_code, }, ) - except AIOGitHubAPIException as exception: - _LOGGER.error(exception) + except GitHubException as exception: + self.hacs.log.error(exception) return self.async_abort(reason="github") return self.async_show_progress_done(next_step_id="device_done") @@ -97,18 +104,12 @@ async def _show_config_form(self, user_input): step_id="user", data_schema=vol.Schema( { - vol.Required( - "acc_logs", default=user_input.get("acc_logs", False) - ): bool, - vol.Required( - "acc_addons", default=user_input.get("acc_addons", False) - ): bool, + vol.Required("acc_logs", default=user_input.get("acc_logs", False)): bool, + vol.Required("acc_addons", default=user_input.get("acc_addons", False)): bool, vol.Required( "acc_untested", default=user_input.get("acc_untested", False) ): bool, - vol.Required( - "acc_disable", default=user_input.get("acc_disable", False) - ): bool, + vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool, } ), errors=self._errors, @@ -116,9 +117,7 @@ async def _show_config_form(self, user_input): async def async_step_device_done(self, _user_input): """Handle device steps""" - return self.async_create_entry( - title="", data={"token": self.activation.access_token} - ) + return self.async_create_entry(title="", data={"token": self.activation.access_token}) @staticmethod @callback @@ -126,7 +125,7 @@ def async_get_options_flow(config_entry): return HacsOptionsFlowHandler(config_entry) -class HacsOptionsFlowHandler(config_entries.OptionsFlow): +class HacsOptionsFlowHandler(HacsMixin, config_entries.OptionsFlow): """HACS config flow options handler.""" def __init__(self, config_entry): @@ -139,14 +138,16 @@ async def async_step_init(self, _user_input=None): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - hacs: HacsBase = get_hacs() if user_input is not None: + limit = int(user_input.get(RELEASE_LIMIT, 5)) + if limit <= 0 or limit > 100: + return self.async_abort(reason="release_limit_value") return self.async_create_entry(title="", data=user_input) - if hacs.configuration is None: + if self.hacs.configuration is None: return self.async_abort(reason="not_setup") - if hacs.configuration.config_type == "yaml": + if self.hacs.configuration.config_type == ConfigurationType.YAML: schema = {vol.Optional("not_in_use", default=""): str} else: schema = hacs_config_option_schema(self.config_entry.options) diff --git a/custom_components/hacs/const.py b/custom_components/hacs/const.py index 815dd7b3..5befadeb 100644 --- a/custom_components/hacs/const.py +++ b/custom_components/hacs/const.py @@ -3,27 +3,21 @@ NAME_LONG = "HACS (Home Assistant Community Store)" NAME_SHORT = "HACS" -INTEGRATION_VERSION = "1.12.3" DOMAIN = "hacs" CLIENT_ID = "395a8e669c5de9f7c6e8" -MINIMUM_HA_VERSION = "2020.12.0" +MINIMUM_HA_VERSION = "2021.2.0" PROJECT_URL = "https://github.com/hacs/integration/" -CUSTOM_UPDATER_LOCATIONS = [ - "{}/custom_components/custom_updater.py", - "{}/custom_components/custom_updater/__init__.py", -] + ISSUE_URL = f"{PROJECT_URL}issues" DOMAIN_DATA = f"{NAME_SHORT.lower()}_data" -ELEMENT_TYPES = ["integration", "plugin"] - PACKAGE_NAME = "custom_components.hacs" -HACS_GITHUB_API_HEADERS = { - "User-Agent": f"HACS/{INTEGRATION_VERSION}", - "Accept": ACCEPT_HEADERS["preview"], -} +REPOSITORY_HACS_DEFAULT = "hacs/default" +REPOSITORY_HACS_INTEGRATION = "hacs/integration" + +PLATFORMS = ["sensor"] HACS_ACTION_GITHUB_API_HEADERS = { "User-Agent": "HACS/action", @@ -44,16 +38,11 @@ # Messages NO_ELEMENTS = "No elements to show, open the store to install some awesome stuff." -CUSTOM_UPDATER_WARNING = """ -This cannot be used with custom_updater. -To use this you need to remove custom_updater form {} -""" - -STARTUP = f""" +STARTUP = """ ------------------------------------------------------------------- HACS (Home Assistant Community Store) -Version: {INTEGRATION_VERSION} +Version: {version} This is a custom integration If you have any issues with this you need to open an issue here: https://github.com/hacs/integration/issues diff --git a/custom_components/hacs/enums.py b/custom_components/hacs/enums.py index 5509bf7d..268cf5e3 100644 --- a/custom_components/hacs/enums.py +++ b/custom_components/hacs/enums.py @@ -13,12 +13,21 @@ class HacsCategory(str, Enum): THEME = "theme" REMOVED = "removed" + def __str__(self): + return str(self.value) + + +class ConfigurationType(str, Enum): + YAML = "yaml" + CONFIG_ENTRY = "config_entry" + class LovelaceMode(str, Enum): """Lovelace Modes.""" STORAGE = "storage" AUTO = "auto" + AUTO_GEN = "auto-gen" YAML = "yaml" diff --git a/custom_components/hacs/helpers/classes/exceptions.py b/custom_components/hacs/exceptions.py similarity index 71% rename from custom_components/hacs/helpers/classes/exceptions.py rename to custom_components/hacs/exceptions.py index 4532ee89..f867d3ee 100644 --- a/custom_components/hacs/helpers/classes/exceptions.py +++ b/custom_components/hacs/exceptions.py @@ -1,4 +1,4 @@ -"""Custom Exceptions.""" +"""Custom Exceptions for HACS.""" class HacsException(Exception): @@ -15,3 +15,7 @@ class HacsNotModifiedException(HacsException): class HacsExpectedException(HacsException): """For stuff that are expected.""" + + +class HacsRepositoryExistException(HacsException): + """For repositories that are already exist.""" diff --git a/custom_components/hacs/hacsbase/configuration.py b/custom_components/hacs/hacsbase/configuration.py deleted file mode 100644 index 825582bb..00000000 --- a/custom_components/hacs/hacsbase/configuration.py +++ /dev/null @@ -1,77 +0,0 @@ -"""HACS Configuration.""" -import attr - -from custom_components.hacs.helpers.classes.exceptions import HacsException -from custom_components.hacs.helpers.functions.logger import getLogger - -_LOGGER = getLogger() - - -@attr.s(auto_attribs=True) -class Configuration: - """Configuration class.""" - - # 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 - debug: bool = False - dev: bool = False - frontend_mode: str = "Grid" - frontend_compact: bool = False - frontend_repo: str = "" - frontend_repo_url: str = "" - options: dict = {} - onboarding_done: bool = False - plugin_path: str = "www/community/" - python_script_path: str = "python_scripts/" - python_script: bool = False - sidepanel_icon: str = "hacs:hacs" - sidepanel_title: str = "HACS" - theme_path: str = "themes/" - theme: bool = False - token: str = None - - # Config options: - country: str = "ALL" - experimental: bool = False - release_limit: int = 5 - - def to_json(self) -> dict: - """Return a dict representation of the configuration.""" - return self.__dict__ - - def print(self) -> None: - """Print the current configuration to the log.""" - config = self.to_json() - for key in config: - if key in ["config", "config_entry", "options", "token"]: - continue - _LOGGER.debug("%s: %s", key, config[key]) - - @staticmethod - def from_dict(configuration: dict, options: dict = None) -> None: - """Set attributes from dicts.""" - if isinstance(options, bool) or isinstance(configuration.get("options"), bool): - raise HacsException("Configuration is not valid.") - - if options is None: - options = {} - - if not configuration: - raise HacsException("Configuration is not valid.") - - config = Configuration() - - config.config = configuration - config.options = options - - for conf_type in [configuration, options]: - for key in conf_type: - setattr(config, key, conf_type[key]) - - return config diff --git a/custom_components/hacs/hacsbase/data.py b/custom_components/hacs/hacsbase/data.py index d05bbb4e..f8cb4b58 100644 --- a/custom_components/hacs/hacsbase/data.py +++ b/custom_components/hacs/hacsbase/data.py @@ -1,20 +1,32 @@ """Data handler for HACS.""" +import asyncio import os -from queueman import QueueManager +from homeassistant.core import callback -from custom_components.hacs.const import INTEGRATION_VERSION from custom_components.hacs.helpers.classes.manifest import HacsManifest -from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.helpers.functions.register_repository import ( register_repository, ) from custom_components.hacs.helpers.functions.store import ( async_load_from_store, async_save_to_store, + async_save_to_store_default_encoder, get_store_for_key, ) from custom_components.hacs.share import get_hacs +from custom_components.hacs.utils.logger import getLogger + + +def update_repository_from_storage(repository, storage_data): + """Merge in data from storage into the repo data.""" + repository.data.memorize_storage(storage_data) + repository.data.update_data(storage_data) + if repository.data.installed: + return + + repository.logger.debug("%s Should be installed but is not... Fixing that!", repository) + repository.data.installed = True class HacsData: @@ -24,7 +36,6 @@ def __init__(self): """Initialize.""" self.logger = getLogger() self.hacs = get_hacs() - self.queue = QueueManager() self.content = {} async def async_write(self): @@ -42,23 +53,23 @@ async def async_write(self): "view": self.hacs.configuration.frontend_mode, "compact": self.hacs.configuration.frontend_compact, "onboarding_done": self.hacs.configuration.onboarding_done, + "archived_repositories": self.hacs.common.archived_repositories, + "renamed_repositories": self.hacs.common.renamed_repositories, }, ) + await self._async_store_content_and_repos() + for event in ("hacs/repository", "hacs/config"): + self.hacs.hass.bus.async_fire(event, {}) + async def _async_store_content_and_repos(self): # bb: ignore + """Store the main repos file and each repo that is out of date.""" # Repositories self.content = {} - for repository in self.hacs.repositories or []: - self.queue.add(self.async_store_repository_data(repository)) - - if not self.queue.has_pending_tasks: - self.logger.debug("Nothing in the queue") - elif self.queue.running: - self.logger.debug("Queue is already running") - else: - await self.queue.execute() + # Not run concurrently since this is bound by disk I/O + for repository in self.hacs.repositories: + await self.async_store_repository_data(repository) + await async_save_to_store(self.hacs.hass, "repositories", self.content) - self.hacs.hass.bus.async_fire("hacs/repository", {}) - self.hacs.hass.bus.async_fire("hacs/config", {}) async def async_store_repository_data(self, repository): repository_manifest = repository.repository_manifest.manifest @@ -85,89 +96,88 @@ async def async_store_repository_data(self, repository): "topics": repository.data.topics, "version_installed": repository.data.installed_version, } - if data: - if repository.data.installed and ( - repository.data.installed_commit or repository.data.installed_version - ): - await async_save_to_store( - self.hacs.hass, - f"hacs/{repository.data.id}.hacs", - repository.data.to_json(), - ) - self.content[str(repository.data.id)] = data + self.content[str(repository.data.id)] = data + + if ( + repository.data.installed + and (repository.data.installed_commit or repository.data.installed_version) + and (export := repository.data.export_data()) + ): + # export_data will return `None` if the memorized + # data is already up to date which allows us to avoid + # writing data that is already up to date or generating + # executor jobs to check the data on disk to see + # if a write is needed. + await async_save_to_store_default_encoder( + self.hacs.hass, + f"hacs/{repository.data.id}.hacs", + export, + ) + repository.data.memorize_storage(export) async def restore(self): """Restore saved data.""" hacs = await async_load_from_store(self.hacs.hass, "hacs") - repositories = await async_load_from_store(self.hacs.hass, "repositories") - try: - if not hacs and not repositories: - # Assume new install - self.hacs.status.new = True - return True - self.logger.info("Restore started") - self.hacs.status.new = False - - # Hacs - 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) + repositories = await async_load_from_store(self.hacs.hass, "repositories") or {} - # Repositories - stores = {} - for entry in repositories or []: - stores[entry] = get_store_for_key(self.hacs.hass, f"hacs/{entry}.hacs") + if not hacs and not repositories: + # Assume new install + self.hacs.status.new = True + return True + self.logger.info("Restore started") + self.hacs.status.new = False - stores_exist = {} + # Hacs + 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) + self.hacs.common.archived_repositories = hacs.get("archived_repositories", []) + self.hacs.common.renamed_repositories = hacs.get("renamed_repositories", {}) - def _populate_stores(): - for entry in repositories or []: - stores_exist[entry] = os.path.exists(stores[entry].path) + # Repositories + hass = self.hacs.hass + stores = {} - await self.hacs.hass.async_add_executor_job(_populate_stores) + try: + await self.register_unknown_repositories(repositories) - # Repositories - for entry in repositories or []: - self.queue.add( - self.async_restore_repository( - entry, repositories[entry], stores[entry], stores_exist[entry] - ) - ) + for entry, repo_data in repositories.items(): + if self.async_restore_repository(entry, repo_data): + stores[entry] = get_store_for_key(hass, f"hacs/{entry}.hacs") - await self.queue.execute() + def _load_from_storage(): + for entry, store in stores.items(): + if os.path.exists(store.path) and (data := store.load()): + update_repository_from_storage(self.hacs.get_by_id(entry), data) + await hass.async_add_executor_job(_load_from_storage) self.logger.info("Restore done") except (Exception, BaseException) as exception: # pylint: disable=broad-except - self.logger.critical(f"[{exception}] Restore Failed!") + self.logger.critical(f"[{exception}] Restore Failed!", exc_info=exception) return False return True - async def async_restore_repository( - self, entry, repository_data, store, store_exists - ): - if not self.hacs.is_known(entry): - await register_repository( - repository_data["full_name"], repository_data["category"], False - ) - repository = [ - x - for x in self.hacs.repositories - if str(x.data.id) == str(entry) - or x.data.full_name == repository_data["full_name"] + async def register_unknown_repositories(self, repositories): + """Registry any unknown repositories.""" + register_tasks = [ + register_repository(repo_data["full_name"], repo_data["category"], False) + for entry, repo_data in repositories.items() + if not self.hacs.is_known(entry) ] - if not repository: - self.logger.error(f"Did not find {repository_data['full_name']} ({entry})") - return - - repository = repository[0] - + if register_tasks: + await asyncio.gather(*register_tasks) + + @callback + def async_restore_repository(self, entry, repository_data): + full_name = repository_data["full_name"] + if not (repository := self.hacs.get_by_name(full_name)): + self.logger.error(f"Did not find {full_name} ({entry})") + return False # Restore repository attributes - repository.data.id = entry + self.hacs.async_set_repository_id(repository, entry) 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.releases.last_release_object_downloads = repository_data.get("downloads") repository.data.last_updated = repository_data.get("last_updated") repository.data.etag_repository = repository_data.get("etag_repository") repository.data.topics = repository_data.get("topics", []) @@ -192,15 +202,7 @@ async def async_restore_repository( repository.status.first_install = False if repository_data["full_name"] == "hacs/integration": - repository.data.installed_version = INTEGRATION_VERSION + repository.data.installed_version = self.hacs.version repository.data.installed = True - restored = store_exists and await store.async_load() or {} - - if restored: - repository.data.update_data(restored) - if not repository.data.installed: - repository.logger.debug( - "Should be installed but is not... Fixing that!" - ) - repository.data.installed = True + return True diff --git a/custom_components/hacs/hacsbase/hacs.py b/custom_components/hacs/hacsbase/hacs.py index 1931d04a..d477b712 100644 --- a/custom_components/hacs/hacsbase/hacs.py +++ b/custom_components/hacs/hacsbase/hacs.py @@ -1,8 +1,8 @@ """Initialize the HACS base.""" -import json from datetime import timedelta -from aiogithubapi import AIOGitHubAPIException +from aiogithubapi import GitHubException +from aiogithubapi.exceptions import GitHubNotModifiedException from queueman import QueueManager from queueman.exceptions import QueueManagerExecutionStillInProgress @@ -13,19 +13,11 @@ from custom_components.hacs.helpers.functions.register_repository import ( register_repository, ) -from custom_components.hacs.helpers.functions.remaining_github_calls import ( - get_fetch_updates_for, -) from custom_components.hacs.helpers.functions.store import ( async_load_from_store, async_save_to_store, ) -from custom_components.hacs.operational.setup_actions.categories import ( - async_setup_extra_stores, -) from custom_components.hacs.share import ( - get_factory, - get_queue, get_removed, is_removed, list_removed_repositories, @@ -33,87 +25,74 @@ from ..base import HacsBase from ..enums import HacsCategory, HacsStage - - -class HacsStatus: - """HacsStatus.""" - - startup = True - new = False - background_task = False - reloading_data = False - upgrading_all = False - - -class HacsFrontend: - """HacsFrontend.""" - - version_running = None - version_available = None - version_expected = None - update_pending = False - - -class HacsCommon: - """Common for HACS.""" - - categories = [] - default = [] - installed = [] - skip = [] - - -class System: - """System info.""" - - status = HacsStatus() - config_path = None - ha_version = None - disabled = False - running = False - lovelace_mode = "storage" +from ..share import get_factory, get_queue class Hacs(HacsBase, HacsHelpers): """The base class of HACS, nested throughout the project.""" - repositories = [] - repo = None - data_repo = None - data = None - status = HacsStatus() - configuration = None - version = None - session = None factory = get_factory() queue = get_queue() - recuring_tasks = [] - common = HacsCommon() + + @property + def repositories(self): + """Return the full repositories list.""" + return self._repositories + + def async_set_repositories(self, repositories): + """Set the list of repositories.""" + self._repositories = [] + self._repositories_by_id = {} + self._repositories_by_full_name = {} + + for repository in repositories: + self.async_add_repository(repository) + + def async_set_repository_id(self, repository, repo_id): + """Update a repository id.""" + existing_repo_id = str(repository.data.id) + if existing_repo_id == repo_id: + return + if existing_repo_id != "0": + raise ValueError( + f"The repo id for {repository.data.full_name_lower} is already set to {existing_repo_id}" + ) + repository.data.id = repo_id + self._repositories_by_id[repo_id] = repository + + def async_add_repository(self, repository): + """Add a repository to the list.""" + if repository.data.full_name_lower in self._repositories_by_full_name: + raise ValueError(f"The repo {repository.data.full_name_lower} is already added") + self._repositories.append(repository) + repo_id = str(repository.data.id) + if repo_id != "0": + self._repositories_by_id[repo_id] = repository + self._repositories_by_full_name[repository.data.full_name_lower] = repository + + def async_remove_repository(self, repository): + """Remove a repository from the list.""" + if repository.data.full_name_lower not in self._repositories_by_full_name: + return + self._repositories.remove(repository) + repo_id = str(repository.data.id) + if repo_id in self._repositories_by_id: + del self._repositories_by_id[repo_id] + del self._repositories_by_full_name[repository.data.full_name_lower] def get_by_id(self, repository_id): """Get repository by ID.""" - try: - for repository in self.repositories: - if str(repository.data.id) == str(repository_id): - return repository - except (Exception, BaseException): # pylint: disable=broad-except - pass - return None + return self._repositories_by_id.get(str(repository_id)) def get_by_name(self, repository_full_name): """Get repository by full_name.""" - try: - repository_full_name_lower = repository_full_name.lower() - for repository in self.repositories: - if repository.data.full_name_lower == repository_full_name_lower: - return repository - except (Exception, BaseException): # pylint: disable=broad-except - pass - return None + if repository_full_name is None: + return None + return self._repositories_by_full_name.get(repository_full_name.lower()) def is_known(self, repository_id): """Return a bool if the repository is known.""" - return str(repository_id) in [str(x.data.id) for x in self.repositories] + return str(repository_id) in self._repositories_by_id @property def sorted_by_name(self): @@ -133,7 +112,6 @@ async def startup_tasks(self, _event=None): """Tasks that are started after startup.""" await self.async_set_stage(HacsStage.STARTUP) self.status.background_task = True - await async_setup_extra_stores() self.hass.bus.async_fire("hacs/status", {}) await self.handle_critical_repositories_startup() @@ -191,9 +169,10 @@ async def handle_critical_repositories(self): was_installed = False try: - critical = await self.data_repo.get_contents("critical") - critical = json.loads(critical.content) - except AIOGitHubAPIException: + critical = await self.async_github_get_hacs_default_file("critical") + except GitHubNotModifiedException: + return + except GitHubException: pass if not critical: @@ -253,15 +232,13 @@ async def prosess_queue(self, _notarealarg=None): self.log.debug("Queue is already running") return - can_update = await get_fetch_updates_for(self.github) + can_update = await self.async_can_update() self.log.debug( "Can update %s repositories, items in queue %s", can_update, self.queue.pending_tasks, ) - if can_update == 0: - self.log.info("HACS is ratelimited, repository updates will resume later.") - else: + if can_update != 0: self.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) try: @@ -280,10 +257,7 @@ async def recurring_tasks_installed(self, _notarealarg=None): for repository in self.repositories: if self.status.startup and repository.data.full_name == "hacs/integration": continue - if ( - repository.data.installed - and repository.data.category in self.common.categories - ): + if repository.data.installed and repository.data.category in self.common.categories: self.queue.add(self.factory.safe_update(repository)) await self.handle_critical_repositories() @@ -295,7 +269,6 @@ async def recurring_tasks_installed(self, _notarealarg=None): async def recurring_tasks_all(self, _notarealarg=None): """Recurring tasks for all repositories.""" self.log.debug("Starting recurring background task for all repositories") - await async_setup_extra_stores() self.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) @@ -349,8 +322,12 @@ async def async_get_category_repositories(self, category: HacsCategory): """Get repositories from category.""" repositories = await async_get_list_from_default(category) for repo in repositories: + if self.common.renamed_repositories.get(repo): + repo = self.common.renamed_repositories[repo] if is_removed(repo): continue + if repo in self.common.archived_repositories: + continue repository = self.get_by_name(repo) if repository is not None: if str(repository.data.id) not in self.common.default: @@ -359,9 +336,3 @@ async def async_get_category_repositories(self, category: HacsCategory): continue continue self.queue.add(self.factory.safe_register(repo, category)) - - async def async_set_stage(self, stage: str) -> None: - """Set the stage of HACS.""" - self.stage = HacsStage(stage) - self.log.info("Stage changed: %s", self.stage) - self.hass.bus.async_fire("hacs/stage", {"stage": self.stage}) diff --git a/custom_components/hacs/helpers/classes/manifest.py b/custom_components/hacs/helpers/classes/manifest.py index f3030100..c0e43b9b 100644 --- a/custom_components/hacs/helpers/classes/manifest.py +++ b/custom_components/hacs/helpers/classes/manifest.py @@ -7,7 +7,7 @@ import attr -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException @attr.s(auto_attribs=True) @@ -38,6 +38,10 @@ def from_dict(manifest: dict): manifest_data.manifest = manifest + if country := manifest.get("country"): + if isinstance(country, str): + manifest["country"] = [country] + for key in manifest: setattr(manifest_data, key, manifest[key]) return manifest_data diff --git a/custom_components/hacs/helpers/classes/repository.py b/custom_components/hacs/helpers/classes/repository.py index 0ff89bdf..555cb70b 100644 --- a/custom_components/hacs/helpers/classes/repository.py +++ b/custom_components/hacs/helpers/classes/repository.py @@ -2,18 +2,15 @@ # pylint: disable=broad-except, no-member import json import os +import shutil import tempfile import zipfile -import shutil from aiogithubapi import AIOGitHubAPIException from queueman import QueueManager +from custom_components.hacs.exceptions import HacsException, HacsNotModifiedException from custom_components.hacs.helpers import RepositoryHelpers -from custom_components.hacs.helpers.classes.exceptions import ( - HacsException, - HacsNotModifiedException, -) from custom_components.hacs.helpers.classes.manifest import HacsManifest from custom_components.hacs.helpers.classes.repositorydata import RepositoryData from custom_components.hacs.helpers.classes.validate import Validate @@ -23,7 +20,6 @@ get_repository, ) from custom_components.hacs.helpers.functions.is_safe_to_remove import is_safe_to_remove -from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.helpers.functions.misc import get_repository_name from custom_components.hacs.helpers.functions.save import async_save_file from custom_components.hacs.helpers.functions.store import async_remove_store @@ -35,6 +31,7 @@ version_to_install, ) from custom_components.hacs.share import get_hacs +from custom_components.hacs.utils.logger import getLogger class RepositoryVersions: @@ -144,6 +141,19 @@ def display_name(self): """Return display name.""" return get_repository_name(self) + @property + def ignored_by_country_configuration(self) -> bool: + """Return True if hidden by country.""" + if self.data.installed: + return False + configuration = self.hacs.configuration.country.lower() + manifest = [entry.lower() for entry in self.repository_manifest.country or []] + if configuration == "all": + return False + if not manifest: + return False + return configuration not in manifest + @property def display_status(self): """Return display_status.""" @@ -259,14 +269,8 @@ async def common_update(self, ignore_issues=False, force=False): # Attach repository current_etag = self.data.etag_repository await common_update_data(self, ignore_issues, force) - if ( - not self.data.installed - and (current_etag == self.data.etag_repository) - and not force - ): - self.logger.debug( - "Did not update %s, content was not modified", self.data.full_name - ) + if not self.data.installed and (current_etag == self.data.etag_repository) and not force: + self.logger.debug("Did not update %s, content was not modified", self.data.full_name) return False # Update last updated @@ -291,9 +295,7 @@ async def download_zip_files(self, validate): contents = False for release in self.releases.objects: - self.logger.info( - "%s ref: %s --- tag: %s.", self, self.ref, release.tag_name - ) + self.logger.info("%s ref: %s --- tag: %s.", self, self.ref, release.tag_name) if release.tag_name == self.ref.split("/")[1]: contents = release.assets @@ -353,9 +355,7 @@ 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]: if self.hacs.system.action: - raise HacsException( - "::error:: No hacs.json file in the root of the repository." - ) + raise HacsException("::error:: No hacs.json file in the root of the repository.") return if self.hacs.system.action: self.logger.info("%s Found hacs.json", self) @@ -364,9 +364,7 @@ async def get_repository_manifest_content(self): try: manifest = await self.repository_object.get_contents("hacs.json", self.ref) - self.repository_manifest = HacsManifest.from_dict( - json.loads(manifest.content) - ) + self.repository_manifest = HacsManifest.from_dict(json.loads(manifest.content)) self.data.update_data(json.loads(manifest.content)) except (AIOGitHubAPIException, Exception) as exception: # Gotta Catch 'Em All if self.hacs.system.action: @@ -384,7 +382,7 @@ def remove(self): self.hacs.common.installed.remove(self.data.id) for repository in self.hacs.repositories: if repository.data.id == self.data.id: - self.hacs.repositories.remove(repository) + self.hacs.async_remove_repository(repository) async def uninstall(self): """Run uninstall tasks.""" @@ -399,9 +397,7 @@ async def uninstall(self): self.pending_restart = True elif self.data.category == "theme": try: - await self.hacs.hass.services.async_call( - "frontend", "reload_themes", {} - ) + await self.hacs.hass.services.async_call("frontend", "reload_themes", {}) except (Exception, BaseException): # pylint: disable=broad-except pass if self.data.full_name in self.hacs.common.installed: @@ -418,7 +414,6 @@ async def uninstall(self): async def remove_local_directory(self): """Check the local directory.""" - import shutil from asyncio import sleep try: @@ -442,9 +437,7 @@ async def remove_local_directory(self): if os.path.exists(local_path): if not is_safe_to_remove(local_path): - self.logger.error( - "%s Path %s is blocked from removal", self, local_path - ) + self.logger.error("%s Path %s is blocked from removal", self, local_path) return False self.logger.debug("%s Removing %s", self, local_path) @@ -461,8 +454,6 @@ async def remove_local_directory(self): ) except (Exception, BaseException) as exception: - self.logger.debug( - "%s Removing %s failed with %s", self, local_path, exception - ) + self.logger.debug("%s Removing %s failed with %s", self, local_path, exception) return False return True diff --git a/custom_components/hacs/helpers/classes/repositorydata.py b/custom_components/hacs/helpers/classes/repositorydata.py index ce8870ac..6011346c 100644 --- a/custom_components/hacs/helpers/classes/repositorydata.py +++ b/custom_components/hacs/helpers/classes/repositorydata.py @@ -1,8 +1,10 @@ """Repository data.""" from datetime import datetime -from typing import List +import json +from typing import List, Optional import attr +from homeassistant.helpers.json import JSONEncoder @attr.s(auto_attribs=True) @@ -51,6 +53,7 @@ class RepositoryData: stargazers_count: int = 0 topics: List[str] = [] zip_release: bool = False + _storage_data: Optional[dict] = None @property def stars(self): @@ -66,64 +69,78 @@ def name(self): def to_json(self): """Export to json.""" - return attr.asdict(self) + return attr.asdict(self, filter=lambda attr, _: attr.name != "_storage_data") + + def memorize_storage(self, data) -> None: + """Memorize the storage data.""" + self._storage_data = data + + def export_data(self) -> Optional[dict]: + """Export to json if the data has changed. + + Returns the data to export if the data needs + to be written. + + Returns None if the data has not changed. + """ + export = json.loads(json.dumps(self.to_json(), cls=JSONEncoder)) + return None if self._storage_data == export else export @staticmethod def create_from_dict(source: dict): """Set attributes from dicts.""" data = RepositoryData() for key in source: - print(key) - if key in data.__dict__: - if key == "pushed_at": - if source[key] == "": - continue - if "Z" in source[key]: - setattr( - data, - key, - datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%SZ"), - ) - else: - setattr( - data, - key, - datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%S"), - ) - elif key == "id": - setattr(data, key, str(source[key])) - elif key == "country": - if isinstance(source[key], str): - setattr(data, key, [source[key]]) - else: - setattr(data, key, source[key]) + if key not in data.__dict__: + continue + if key == "pushed_at": + if source[key] == "": + continue + if "Z" in source[key]: + setattr( + data, + key, + datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%SZ"), + ) + else: + setattr( + data, + key, + datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%S"), + ) + elif key == "id": + setattr(data, key, str(source[key])) + elif key == "country": + 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": - if data[key] == "": - continue - if "Z" in data[key]: - setattr( - self, - key, - datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%SZ"), - ) - else: - setattr( - self, key, datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%S") - ) - elif key == "id": - setattr(self, key, str(data[key])) - elif key == "country": - if isinstance(data[key], str): - setattr(self, key, [data[key]]) - else: - setattr(self, key, data[key]) + if key not in self.__dict__: + continue + if key == "pushed_at": + if data[key] == "": + continue + if "Z" in data[key]: + setattr( + self, + key, + datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%SZ"), + ) + else: + setattr(self, key, datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%S")) + elif key == "id": + setattr(self, key, str(data[key])) + elif key == "country": + if isinstance(data[key], str): + setattr(self, key, [data[key]]) else: setattr(self, key, data[key]) + else: + setattr(self, key, data[key]) diff --git a/custom_components/hacs/helpers/functions/constrains.py b/custom_components/hacs/helpers/functions/constrains.py deleted file mode 100644 index 93b4e070..00000000 --- a/custom_components/hacs/helpers/functions/constrains.py +++ /dev/null @@ -1,43 +0,0 @@ -"""HACS Startup constrains.""" -# pylint: disable=bad-continuation -import os - -from custom_components.hacs.const import ( - CUSTOM_UPDATER_LOCATIONS, - CUSTOM_UPDATER_WARNING, - MINIMUM_HA_VERSION, -) -from custom_components.hacs.helpers.functions.misc import version_left_higher_then_right -from custom_components.hacs.share import get_hacs - - -def check_constrains(): - """Check HACS constrains.""" - if not constrain_custom_updater(): - return False - if not constrain_version(): - return False - return True - - -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.core.config_path)): - msg = CUSTOM_UPDATER_WARNING.format(location.format(hacs.core.config_path)) - hacs.log.critical(msg) - return False - return True - - -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.log.critical( - "You need HA version %s or newer to use this integration.", - MINIMUM_HA_VERSION, - ) - return False - return True diff --git a/custom_components/hacs/helpers/functions/download.py b/custom_components/hacs/helpers/functions/download.py index 18851bfb..d5901068 100644 --- a/custom_components/hacs/helpers/functions/download.py +++ b/custom_components/hacs/helpers/functions/download.py @@ -8,13 +8,13 @@ import backoff from queueman import QueueManager, concurrent -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.functions.filters import ( filter_content_return_one_of_type, ) -from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.helpers.functions.save import async_save_file from custom_components.hacs.share import get_hacs +from custom_components.hacs.utils.logger import getLogger _LOGGER = getLogger() @@ -47,11 +47,7 @@ async def async_download_file(url): if request.status == 200: result = await request.read() else: - raise HacsException( - "Got status code {} when trying to download {}".format( - request.status, url - ) - ) + raise HacsException(f"Got status code {request.status} when trying to download {url}") return result @@ -92,18 +88,14 @@ def gather_files_to_download(repository): for treefile in tree: if treefile.filename == repository.data.file_name: files.append( - FileInformation( - treefile.download_url, treefile.full_path, treefile.filename - ) + FileInformation(treefile.download_url, treefile.full_path, treefile.filename) ) return files if category == "plugin": for treefile in tree: if treefile.path in ["", "dist"]: - if remotelocation == "dist" and not treefile.filename.startswith( - "dist" - ): + if remotelocation == "dist" and not treefile.filename.startswith("dist"): continue if not remotelocation: if not treefile.filename.endswith(".js"): @@ -122,17 +114,13 @@ def gather_files_to_download(repository): if repository.data.content_in_root: if not repository.data.filename: if category == "theme": - tree = filter_content_return_one_of_type( - repository.tree, "", "yaml", "full_path" - ) + tree = filter_content_return_one_of_type(repository.tree, "", "yaml", "full_path") for path in tree: if path.is_directory: continue if path.full_path.startswith(repository.content.path.remote): - files.append( - FileInformation(path.download_url, path.full_path, path.filename) - ) + files.append(FileInformation(path.download_url, path.full_path, path.filename)) return files @@ -142,9 +130,7 @@ async def download_zip_files(repository, validate): queue = QueueManager() try: for release in repository.releases.objects: - repository.logger.info( - f"ref: {repository.ref} --- tag: {release.tag_name}" - ) + repository.logger.info(f"ref: {repository.ref} --- tag: {release.tag_name}") if release.tag_name == repository.ref.split("/")[1]: contents = release.assets @@ -210,37 +196,39 @@ async def download_content(repository): @concurrent(10) async def dowload_repository_content(repository, content): """Download content.""" - repository.logger.debug(f"Downloading {content.name}") + try: + repository.logger.debug(f"Downloading {content.name}") - filecontent = await async_download_file(content.download_url) + filecontent = await async_download_file(content.download_url) - if filecontent is None: - repository.validate.errors.append(f"[{content.name}] was not downloaded.") - return + if filecontent is None: + repository.validate.errors.append(f"[{content.name}] was not downloaded.") + return - # Save the content of the file. - if repository.content.single or content.path is None: - local_directory = repository.content.path.local + # Save the content of the file. + if repository.content.single or content.path is None: + local_directory = repository.content.path.local - else: - _content_path = content.path - if not repository.data.content_in_root: - _content_path = _content_path.replace( - f"{repository.content.path.remote}", "" - ) + else: + _content_path = content.path + if not repository.data.content_in_root: + _content_path = _content_path.replace(f"{repository.content.path.remote}", "") - local_directory = f"{repository.content.path.local}/{_content_path}" - local_directory = local_directory.split("/") - del local_directory[-1] - local_directory = "/".join(local_directory) + 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) + # Check local directory + pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) - local_file_path = (f"{local_directory}/{content.name}").replace("//", "/") + 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} completed") - return - repository.validate.errors.append(f"[{content.name}] was not downloaded.") + result = await async_save_file(local_file_path, filecontent) + if result: + repository.logger.info(f"Download of {content.name} completed") + return + repository.validate.errors.append(f"[{content.name}] was not downloaded.") + + except (Exception, BaseException) as exception: # pylint: disable=broad-except + repository.validate.errors.append(f"Download was not completed [{exception}]") diff --git a/custom_components/hacs/helpers/functions/filters.py b/custom_components/hacs/helpers/functions/filters.py index 4f984911..75055e95 100644 --- a/custom_components/hacs/helpers/functions/filters.py +++ b/custom_components/hacs/helpers/functions/filters.py @@ -1,9 +1,7 @@ """Filter functions.""" -def filter_content_return_one_of_type( - content, namestartswith, filterfiltype, attr="name" -): +def filter_content_return_one_of_type(content, namestartswith, filterfiltype, attr="name"): """Only match 1 of the filter.""" contents = [] filetypefound = False diff --git a/custom_components/hacs/helpers/functions/get_list_from_default.py b/custom_components/hacs/helpers/functions/get_list_from_default.py index 3b4e5ba5..3f9d4185 100644 --- a/custom_components/hacs/helpers/functions/get_list_from_default.py +++ b/custom_components/hacs/helpers/functions/get_list_from_default.py @@ -1,11 +1,14 @@ """Helper to get default repositories.""" -import json from typing import List -from aiogithubapi import AIOGitHubAPIException +from aiogithubapi import ( + GitHubAuthenticationException, + GitHubNotModifiedException, + GitHubRatelimitException, +) -from custom_components.hacs.enums import HacsCategory -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.const import REPOSITORY_HACS_DEFAULT +from custom_components.hacs.enums import HacsCategory, HacsDisabledReason from custom_components.hacs.share import get_hacs @@ -15,17 +18,20 @@ async def async_get_list_from_default(default: HacsCategory) -> List: repositories = [] try: - content = await hacs.data_repo.get_contents( - default, hacs.data_repo.default_branch - ) - repositories = json.loads(content.content) + repositories = await hacs.async_github_get_hacs_default_file(default) + hacs.log.debug("Got %s elements for %s", len(repositories), default) + except GitHubNotModifiedException: + hacs.log.debug("Content did not change for %s/%s", REPOSITORY_HACS_DEFAULT, default) - except (AIOGitHubAPIException, HacsException) as exception: + except GitHubRatelimitException as exception: hacs.log.error(exception) + hacs.disable_hacs(HacsDisabledReason.RATE_LIMIT) - except (Exception, BaseException) as exception: + except GitHubAuthenticationException as exception: hacs.log.error(exception) + hacs.disable_hacs(HacsDisabledReason.INVALID_TOKEN) - hacs.log.debug("Got %s elements for %s", len(repositories), default) + except BaseException as exception: # pylint: disable=broad-except + hacs.log.error(exception) return repositories diff --git a/custom_components/hacs/helpers/functions/information.py b/custom_components/hacs/helpers/functions/information.py index 4e9b8415..ba0d6f96 100644 --- a/custom_components/hacs/helpers/functions/information.py +++ b/custom_components/hacs/helpers/functions/information.py @@ -1,15 +1,12 @@ """Return repository information if any.""" import json -from aiogithubapi import AIOGitHubAPIException, GitHub, AIOGitHubAPINotModifiedException +from aiogithubapi import AIOGitHubAPIException, AIOGitHubAPINotModifiedException, GitHub +from aiogithubapi.const import ACCEPT_HEADERS -from custom_components.hacs.helpers.classes.exceptions import ( - HacsException, - HacsNotModifiedException, -) +from custom_components.hacs.exceptions import HacsException, HacsNotModifiedException from custom_components.hacs.helpers.functions.template import render_template from custom_components.hacs.share import get_hacs -from custom_components.hacs.const import HACS_GITHUB_API_HEADERS def info_file(repository): @@ -48,11 +45,15 @@ async def get_info_md_content(repository): async def get_repository(session, token, repository_full_name, etag=None): """Return a repository object or None.""" + hacs = get_hacs() try: github = GitHub( token, session, - headers=HACS_GITHUB_API_HEADERS, + headers={ + "User-Agent": f"HACS/{hacs.version}", + "Accept": ACCEPT_HEADERS["preview"], + }, ) repository = await github.get_repo(repository_full_name, etag) return repository, github.client.last_response.etag @@ -95,9 +96,7 @@ def read_hacs_manifest(): """Reads the HACS manifest file and returns the contents.""" hacs = get_hacs() content = {} - with open( - f"{hacs.core.config_path}/custom_components/hacs/manifest.json" - ) as manifest: + with open(f"{hacs.core.config_path}/custom_components/hacs/manifest.json") as manifest: content = json.loads(manifest.read()) return content @@ -111,9 +110,7 @@ async def get_integration_manifest(repository): 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 = await repository.repository_object.get_contents(manifest_path, repository.ref) manifest = json.loads(manifest.content) except (Exception, BaseException) as exception: # pylint: disable=broad-except raise HacsException(f"Could not read manifest.json [{exception}]") @@ -197,9 +194,7 @@ def get_file_name_plugin(repository): else: for filename in valid_filenames: - if f"{location+'/' if location else ''}{filename}" in [ - x.full_path for x in tree - ]: + 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 diff --git a/custom_components/hacs/helpers/functions/is_safe_to_remove.py b/custom_components/hacs/helpers/functions/is_safe_to_remove.py index 4de39662..ca54f18f 100644 --- a/custom_components/hacs/helpers/functions/is_safe_to_remove.py +++ b/custom_components/hacs/helpers/functions/is_safe_to_remove.py @@ -1,20 +1,10 @@ """Helper to check if path is safe to remove.""" -from pathlib import Path - from custom_components.hacs.share import get_hacs +from ...utils.path import is_safe + def is_safe_to_remove(path: str) -> bool: """Helper to check if path is safe to remove.""" hacs = get_hacs() - paths = [ - Path(f"{hacs.core.config_path}/{hacs.configuration.appdaemon_path}"), - Path(f"{hacs.core.config_path}/{hacs.configuration.netdaemon_path}"), - Path(f"{hacs.core.config_path}/{hacs.configuration.plugin_path}"), - Path(f"{hacs.core.config_path}/{hacs.configuration.python_script_path}"), - Path(f"{hacs.core.config_path}/{hacs.configuration.theme_path}"), - Path(f"{hacs.core.config_path}/custom_components/"), - ] - if Path(path) in paths: - return False - return True + return is_safe(hacs, path) diff --git a/custom_components/hacs/helpers/functions/misc.py b/custom_components/hacs/helpers/functions/misc.py index 5114d6b5..e31977b8 100644 --- a/custom_components/hacs/helpers/functions/misc.py +++ b/custom_components/hacs/helpers/functions/misc.py @@ -1,8 +1,7 @@ """Helper functions: misc""" import re -from functools import lru_cache -from awesomeversion import AwesomeVersion +from ...utils import version RE_REPOSITORY = re.compile( r"(?:(?:.*github.com.)|^)([A-Za-z0-9-]+\/[\w.-]+?)(?:(?:\.git)?|(?:[^\w.-].*)?)$" @@ -20,18 +19,12 @@ def get_repository_name(repository) -> str: if "name" in repository.integration_manifest: return repository.integration_manifest["name"] - return ( - repository.data.full_name.split("/")[-1] - .replace("-", " ") - .replace("_", " ") - .title() - ) + return repository.data.full_name.split("/")[-1].replace("-", " ").replace("_", " ").title() -@lru_cache(maxsize=1024) def version_left_higher_then_right(left: str, right: str) -> bool: """Return a bool if source is newer than target, will also be true if identical.""" - return AwesomeVersion(left) >= AwesomeVersion(right) + return version.version_left_higher_then_right(left, right) def extract_repository_from_url(url: str) -> str or None: diff --git a/custom_components/hacs/helpers/functions/register_repository.py b/custom_components/hacs/helpers/functions/register_repository.py index 2fad499a..ef5f2e69 100644 --- a/custom_components/hacs/helpers/functions/register_repository.py +++ b/custom_components/hacs/helpers/functions/register_repository.py @@ -1,14 +1,21 @@ """Register a repository.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + from aiogithubapi import AIOGitHubAPIException -from custom_components.hacs.helpers.classes.exceptions import ( +from custom_components.hacs.exceptions import ( HacsException, HacsExpectedException, + HacsRepositoryExistException, ) from custom_components.hacs.share import get_hacs from ...repositories import RERPOSITORY_CLASSES +if TYPE_CHECKING: + from ..classes.repository import HacsRepository # @concurrent(15, 5) async def register_repository(full_name, category, check=True, ref=None): @@ -22,7 +29,7 @@ async def register_repository(full_name, category, check=True, ref=None): if category not in RERPOSITORY_CLASSES: raise HacsException(f"{category} is not a valid repository category.") - repository = RERPOSITORY_CLASSES[category](full_name) + repository: HacsRepository = RERPOSITORY_CLASSES[category](full_name) if check: try: await repository.async_registration(ref) @@ -39,26 +46,17 @@ async def register_repository(full_name, category, check=True, ref=None): repository.logger.info("%s Validation completed", repository) else: repository.logger.info("%s Registration completed", repository) + except HacsRepositoryExistException: + return except AIOGitHubAPIException as exception: hacs.common.skip.append(repository.data.full_name) - raise HacsException( - f"Validation for {full_name} failed with {exception}." - ) from None - - exists = ( - False - if str(repository.data.id) == "0" - else [x for x in hacs.repositories if str(x.data.id) == str(repository.data.id)] - ) + raise HacsException(f"Validation for {full_name} failed with {exception}.") from None - if exists: - if exists[0] in hacs.repositories: - hacs.repositories.remove(exists[0]) + if str(repository.data.id) != "0" and (exists := hacs.get_by_id(repository.data.id)): + hacs.async_remove_repository(exists) else: - if hacs.hass is not None and ( - (check and repository.data.new) or hacs.status.new - ): + if hacs.hass is not None and ((check and repository.data.new) or hacs.status.new): hacs.hass.bus.async_fire( "hacs/repository", { @@ -67,4 +65,4 @@ async def register_repository(full_name, category, check=True, ref=None): "repository_id": repository.data.id, }, ) - hacs.repositories.append(repository) + hacs.async_add_repository(repository) diff --git a/custom_components/hacs/helpers/functions/remaining_github_calls.py b/custom_components/hacs/helpers/functions/remaining_github_calls.py deleted file mode 100644 index 27a0afe7..00000000 --- a/custom_components/hacs/helpers/functions/remaining_github_calls.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Helper to calculate the remaining calls to github.""" -import math - -from custom_components.hacs.helpers.functions.logger import getLogger - -_LOGGER = getLogger() - - -async def remaining(github): - """Helper to calculate the remaining calls to github.""" - try: - ratelimits = await github.get_rate_limit() - except (BaseException, Exception) as exception: # pylint: disable=broad-except - _LOGGER.error(exception) - return None - if ratelimits.get("remaining") is not None: - return int(ratelimits["remaining"]) - return 0 - - -async def get_fetch_updates_for(github): - """Helper to calculate the number of repositories we can fetch data for.""" - margin = 1000 - limit = await remaining(github) - pr_repo = 15 - - if limit is None: - return None - - if limit - margin <= pr_repo: - return 0 - return math.floor((limit - margin) / pr_repo) diff --git a/custom_components/hacs/helpers/functions/save.py b/custom_components/hacs/helpers/functions/save.py index 05a6e03f..c93a5f60 100644 --- a/custom_components/hacs/helpers/functions/save.py +++ b/custom_components/hacs/helpers/functions/save.py @@ -5,7 +5,7 @@ import aiofiles -from custom_components.hacs.helpers.functions.logger import getLogger +from custom_components.hacs.utils.logger import getLogger _LOGGER = getLogger() @@ -23,9 +23,7 @@ async def async_save_file(location, content): errors = None try: - async with aiofiles.open( - location, mode=mode, encoding=encoding, errors=errors - ) as outfile: + async with aiofiles.open(location, mode=mode, encoding=encoding, errors=errors) as outfile: await outfile.write(content) outfile.close() diff --git a/custom_components/hacs/helpers/functions/store.py b/custom_components/hacs/helpers/functions/store.py index 04c919af..b9e0f335 100644 --- a/custom_components/hacs/helpers/functions/store.py +++ b/custom_components/hacs/helpers/functions/store.py @@ -1,44 +1,79 @@ """Storage handers.""" # pylint: disable=import-outside-toplevel from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.storage import Store +from homeassistant.util import json as json_util from custom_components.hacs.const import VERSION_STORAGE -from .logger import getLogger + +from ...utils.logger import getLogger _LOGGER = getLogger() -def get_store_for_key(hass, key): +class HACSStore(Store): + """A subclass of Store that allows multiple loads in the executor.""" + + def load(self): + """Load the data from disk if version matches.""" + data = json_util.load_json(self.path) + if data == {} or data["version"] != self.version: + return None + return data["data"] + + +def get_store_key(key): + """Return the key to use with homeassistant.helpers.storage.Storage.""" + return key if "/" in key else f"hacs.{key}" + + +def _get_store_for_key(hass, key, encoder): """Create a Store object for the key.""" - key = key if "/" in key else f"hacs.{key}" - from homeassistant.helpers.storage import Store + return HACSStore(hass, VERSION_STORAGE, get_store_key(key), encoder=encoder) + - return Store(hass, VERSION_STORAGE, key, encoder=JSONEncoder) +def get_store_for_key(hass, key): + """Create a Store object for the key.""" + return _get_store_for_key(hass, key, JSONEncoder) async def async_load_from_store(hass, key): """Load the retained data from store and return de-serialized data.""" - store = get_store_for_key(hass, key) - restored = await store.async_load() - if restored is None: - return {} - return restored + return await get_store_for_key(hass, key).async_load() or {} + + +async def async_save_to_store_default_encoder(hass, key, data): + """Generate store json safe data to the filesystem. + + The data is expected to be encodable with the default + python json encoder. It should have already been passed through + JSONEncoder if needed. + """ + await _get_store_for_key(hass, key, None).async_save(data) async def async_save_to_store(hass, key, data): - """Generate dynamic data to store and save it to the filesystem.""" + """Generate dynamic data to store and save it to the filesystem. + + The data is only written if the content on the disk has changed + by reading the existing content and comparing it. + + If the data has changed this will generate two executor jobs + + If the data has not changed this will generate one executor job + """ current = await async_load_from_store(hass, key) if current is None or current != data: await get_store_for_key(hass, key).async_save(data) return _LOGGER.debug( "Did not store data for '%s'. Content did not change", - key if "/" in key else f"hacs.{key}", + get_store_key(key), ) async def async_remove_store(hass, key): - """Remove a store element that should no longer be used""" + """Remove a store element that should no longer be used.""" if "/" not in key: return await get_store_for_key(hass, key).async_remove() diff --git a/custom_components/hacs/helpers/functions/template.py b/custom_components/hacs/helpers/functions/template.py index 4f59840f..a7f16edf 100644 --- a/custom_components/hacs/helpers/functions/template.py +++ b/custom_components/hacs/helpers/functions/template.py @@ -2,7 +2,7 @@ # pylint: disable=broad-except from jinja2 import Template -from custom_components.hacs.helpers.functions.logger import getLogger +from custom_components.hacs.utils.logger import getLogger _LOGGER = getLogger() diff --git a/custom_components/hacs/helpers/functions/validate_repository.py b/custom_components/hacs/helpers/functions/validate_repository.py index 7a88d706..815217c6 100644 --- a/custom_components/hacs/helpers/functions/validate_repository.py +++ b/custom_components/hacs/helpers/functions/validate_repository.py @@ -1,10 +1,15 @@ """Helper to do common validation for repositories.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + from aiogithubapi import AIOGitHubAPIException -from custom_components.hacs.helpers.classes.exceptions import ( +from custom_components.hacs.exceptions import ( HacsException, HacsNotModifiedException, HacsRepositoryArchivedException, + HacsRepositoryExistException, ) from custom_components.hacs.helpers.functions.information import ( get_releases, @@ -16,6 +21,9 @@ ) from custom_components.hacs.share import get_hacs, is_removed +if TYPE_CHECKING: + from custom_components.hacs.helpers.classes.repository import HacsRepository + async def common_validate(repository, ignore_issues=False): """Common validation steps of the repository.""" @@ -29,7 +37,7 @@ async def common_validate(repository, ignore_issues=False): await repository.get_repository_manifest_content() -async def common_update_data(repository, ignore_issues=False, force=False): +async def common_update_data(repository: HacsRepository, ignore_issues=False, force=False): """Common update data.""" hacs = get_hacs() releases = [] @@ -38,15 +46,22 @@ async def common_update_data(repository, ignore_issues=False, force=False): hacs.session, hacs.configuration.token, repository.data.full_name, - etag=None - if force or repository.data.installed - else repository.data.etag_repository, + etag=None if force or repository.data.installed else repository.data.etag_repository, ) repository.repository_object = repository_object + if repository.data.full_name.lower() != repository_object.full_name.lower(): + hacs.common.renamed_repositories[ + repository.data.full_name + ] = repository_object.full_name + if str(repository_object.id) not in hacs.common.default: + hacs.common.default.append(str(repository_object.id)) + raise HacsRepositoryExistException repository.data.update_data(repository_object.attributes) repository.data.etag_repository = etag except HacsNotModifiedException: return + except HacsRepositoryExistException: + raise HacsRepositoryExistException from None except (AIOGitHubAPIException, HacsException) as exception: if not hacs.status.startup: repository.logger.error("%s %s", repository, exception) @@ -57,6 +72,7 @@ async def common_update_data(repository, ignore_issues=False, force=False): # Make sure the repository is not archived. if repository.data.archived and not ignore_issues: repository.validate.errors.append("Repository is archived.") + hacs.common.archived_repositories.append(repository.data.full_name) raise HacsRepositoryArchivedException("Repository is archived.") # Make sure the repository is not in the blacklist. @@ -74,9 +90,7 @@ async def common_update_data(repository, ignore_issues=False, force=False): if releases: repository.data.releases = True repository.releases.objects = [x for x in releases if not x.draft] - repository.data.published_tags = [ - x.tag_name for x in repository.releases.objects - ] + repository.data.published_tags = [x.tag_name for x in repository.releases.objects] repository.data.last_version = next(iter(repository.data.published_tags)) except (AIOGitHubAPIException, HacsException): diff --git a/custom_components/hacs/helpers/methods/installation.py b/custom_components/hacs/helpers/methods/installation.py index 92205826..7e152177 100644 --- a/custom_components/hacs/helpers/methods/installation.py +++ b/custom_components/hacs/helpers/methods/installation.py @@ -1,9 +1,9 @@ # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from abc import ABC import os import tempfile -from abc import ABC -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.functions.download import download_content from custom_components.hacs.helpers.functions.version_to_install import ( version_to_install, @@ -56,9 +56,7 @@ async def async_install_repository(repository): repository.validate.errors = [] if not repository.can_install: - raise HacsException( - "The version of Home Assistant is not compatible with this version" - ) + raise HacsException("The version of Home Assistant is not compatible with this version") version = version_to_install(repository) if version == repository.data.default_branch: @@ -67,9 +65,7 @@ async def async_install_repository(repository): repository.ref = f"tags/{version}" if repository.data.installed and repository.data.category == "netdaemon": - persistent_directory = await hacs.hass.async_add_executor_job( - BackupNetDaemon, repository - ) + persistent_directory = await hacs.hass.async_add_executor_job(BackupNetDaemon, repository) await hacs.hass.async_add_executor_job(persistent_directory.create) elif repository.data.persistent_directory: @@ -87,7 +83,7 @@ async def async_install_repository(repository): await hacs.hass.async_add_executor_job(backup.create) if repository.data.zip_release and version != repository.data.default_branch: - await repository.download_zip_files(repository) + await repository.download_zip_files(repository.validate) else: await download_content(repository) diff --git a/custom_components/hacs/helpers/methods/registration.py b/custom_components/hacs/helpers/methods/registration.py index d6f22887..71eed45c 100644 --- a/custom_components/hacs/helpers/methods/registration.py +++ b/custom_components/hacs/helpers/methods/registration.py @@ -11,9 +11,7 @@ async def async_pre_registration(self): class RepositoryMethodRegistration(ABC): async def registration(self, ref=None) -> None: - self.logger.warning( - "'registration' is deprecated, use 'async_registration' instead" - ) + self.logger.warning("'registration' is deprecated, use 'async_registration' instead") await self.async_registration(ref) async def async_registration(self, ref=None) -> None: diff --git a/custom_components/hacs/helpers/properties/can_be_installed.py b/custom_components/hacs/helpers/properties/can_be_installed.py index df6b4d64..6ec5df62 100644 --- a/custom_components/hacs/helpers/properties/can_be_installed.py +++ b/custom_components/hacs/helpers/properties/can_be_installed.py @@ -10,7 +10,7 @@ def can_be_installed(self) -> bool: if self.data.homeassistant is not None: if self.data.releases: if not version_left_higher_then_right( - self.hacs.system.ha_version, self.data.homeassistant + self.hacs.core.ha_version, self.data.homeassistant ): return False return True diff --git a/custom_components/hacs/manifest.json b/custom_components/hacs/manifest.json index f7140773..5fe79853 100644 --- a/custom_components/hacs/manifest.json +++ b/custom_components/hacs/manifest.json @@ -17,11 +17,11 @@ "name": "HACS", "requirements": [ "aiofiles>=0.6.0", - "aiogithubapi>=21.4.0", + "aiogithubapi>=21.8.1", "awesomeversion>=21.2.2", "backoff>=1.10.0", - "hacs_frontend==20210429001005", + "hacs_frontend==20210822172723", "queueman==0.5" ], - "version": "1.12.3" + "version": "1.15.2" } \ No newline at end of file diff --git a/custom_components/hacs/mixin.py b/custom_components/hacs/mixin.py new file mode 100644 index 00000000..3283e8c3 --- /dev/null +++ b/custom_components/hacs/mixin.py @@ -0,0 +1,24 @@ +"""Mixin classes.""" +# pylint: disable=too-few-public-methods +from __future__ import annotations + +from logging import Logger +from typing import TYPE_CHECKING + +from .share import get_hacs +from .utils.logger import getLogger + +if TYPE_CHECKING: + from .hacsbase.hacs import Hacs + + +class HacsMixin: + """Mixin to provide 'self.hacs' to classes.""" + + hacs: Hacs = get_hacs() + + +class LogMixin: + """Mixin to provide 'self.log' to classes.""" + + log: Logger = getLogger() diff --git a/custom_components/hacs/models/__init__.py b/custom_components/hacs/models/__init__.py deleted file mode 100644 index 3fb384a2..00000000 --- a/custom_components/hacs/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Hacs models.""" diff --git a/custom_components/hacs/models/core.py b/custom_components/hacs/models/core.py deleted file mode 100644 index 1cffaa5a..00000000 --- a/custom_components/hacs/models/core.py +++ /dev/null @@ -1,15 +0,0 @@ -"""HACS Core info.""" -from pathlib import Path - -import attr - -from ..enums import LovelaceMode - - -@attr.s -class HacsCore: - """HACS Core info.""" - - config_path = attr.ib(Path) - ha_version = attr.ib(str) - lovelace_mode = LovelaceMode("storage") diff --git a/custom_components/hacs/models/frontend.py b/custom_components/hacs/models/frontend.py deleted file mode 100644 index 1b9d875d..00000000 --- a/custom_components/hacs/models/frontend.py +++ /dev/null @@ -1,10 +0,0 @@ -"""HacsFrontend.""" - - -class HacsFrontend: - """HacsFrontend.""" - - version_running: bool = None - version_available: bool = None - version_expected: bool = None - update_pending: bool = False diff --git a/custom_components/hacs/models/system.py b/custom_components/hacs/models/system.py deleted file mode 100644 index 4923c624..00000000 --- a/custom_components/hacs/models/system.py +++ /dev/null @@ -1,18 +0,0 @@ -"""HACS System info.""" -from typing import Optional -import attr - -from ..const import INTEGRATION_VERSION -from ..enums import HacsStage - - -@attr.s -class HacsSystem: - """HACS System info.""" - - disabled: bool = False - disabled_reason: Optional[str] = None - running: bool = False - version: str = INTEGRATION_VERSION - stage: HacsStage = attr.ib(HacsStage) - action: bool = False diff --git a/custom_components/hacs/operational/backup.py b/custom_components/hacs/operational/backup.py index 4edf8eb3..b1770209 100644 --- a/custom_components/hacs/operational/backup.py +++ b/custom_components/hacs/operational/backup.py @@ -5,7 +5,7 @@ from time import sleep from custom_components.hacs.helpers.functions.is_safe_to_remove import is_safe_to_remove -from custom_components.hacs.helpers.functions.logger import getLogger +from custom_components.hacs.utils.logger import getLogger BACKUP_PATH = tempfile.gettempdir() + "/hacs_backup/" @@ -65,9 +65,7 @@ def restore(self): while os.path.exists(self.local_path): sleep(0.1) shutil.copytree(self.backup_path_full, self.local_path) - _LOGGER.debug( - "Restored %s, from backup %s", self.local_path, self.backup_path_full - ) + _LOGGER.debug("Restored %s, from backup %s", self.local_path, self.backup_path_full) def cleanup(self): """Cleanup backup files.""" @@ -110,9 +108,7 @@ def restore(self): 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}" - ) + target_file_name = f"{self.repository.content.path.local}/{filename}" shutil.copyfile(source_file_name, target_file_name) def cleanup(self): diff --git a/custom_components/hacs/operational/factory.py b/custom_components/hacs/operational/factory.py index f40de663..5e045c52 100644 --- a/custom_components/hacs/operational/factory.py +++ b/custom_components/hacs/operational/factory.py @@ -3,15 +3,15 @@ from aiogithubapi import AIOGitHubAPIException -from custom_components.hacs.helpers.classes.exceptions import ( +from custom_components.hacs.exceptions import ( HacsException, HacsNotModifiedException, HacsRepositoryArchivedException, ) -from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.helpers.functions.register_repository import ( register_repository, ) +from custom_components.hacs.utils.logger import getLogger max_concurrent_tasks = asyncio.Semaphore(15) sleeper = 5 diff --git a/custom_components/hacs/operational/reload.py b/custom_components/hacs/operational/reload.py deleted file mode 100644 index 037887e6..00000000 --- a/custom_components/hacs/operational/reload.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Reload HACS""" - - -async def async_reload_entry(hass, config_entry): - """Reload HACS.""" - from custom_components.hacs.operational.remove import async_remove_entry - from custom_components.hacs.operational.setup import async_setup_entry - - await async_remove_entry(hass, config_entry) - await async_setup_entry(hass, config_entry) diff --git a/custom_components/hacs/operational/remove.py b/custom_components/hacs/operational/remove.py deleted file mode 100644 index b09c5477..00000000 --- a/custom_components/hacs/operational/remove.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Remove HACS.""" -from ..const import DOMAIN -from ..enums import HacsDisabledReason -from ..share import get_hacs - - -async def async_remove_entry(hass, config_entry): - """Handle removal of an entry.""" - hacs = get_hacs() - hacs.log.info("Disabling HACS") - hacs.log.info("Removing recurring tasks") - for task in hacs.recuring_tasks: - task() - if config_entry.state == "loaded": - hacs.log.info("Removing sensor") - try: - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - except ValueError: - pass - try: - if "hacs" in hass.data.get("frontend_panels", {}): - hacs.log.info("Removing sidepanel") - hass.components.frontend.async_remove_panel("hacs") - except AttributeError: - pass - if DOMAIN in hass.data: - del hass.data[DOMAIN] - hacs.disable(HacsDisabledReason.REMOVED) diff --git a/custom_components/hacs/operational/runtime.py b/custom_components/hacs/operational/runtime.py deleted file mode 100644 index 95f5985b..00000000 --- a/custom_components/hacs/operational/runtime.py +++ /dev/null @@ -1 +0,0 @@ -"""Runtime...""" diff --git a/custom_components/hacs/operational/setup.py b/custom_components/hacs/operational/setup.py index c355e081..14a46baa 100644 --- a/custom_components/hacs/operational/setup.py +++ b/custom_components/hacs/operational/setup.py @@ -1,42 +1,19 @@ """Setup HACS.""" -from datetime import datetime -from aiogithubapi import AIOGitHubAPIException, GitHub -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.const import __version__ as HAVERSION -from homeassistant.core import CoreState +from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI +from aiogithubapi.const import ACCEPT_HEADERS +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, __version__ as HAVERSION +from homeassistant.core import CoreState, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.event import async_call_later +from homeassistant.loader import async_get_integration -from custom_components.hacs.const import ( - DOMAIN, - HACS_GITHUB_API_HEADERS, - INTEGRATION_VERSION, - STARTUP, -) -from custom_components.hacs.enums import HacsDisabledReason, HacsStage -from custom_components.hacs.hacsbase.configuration import Configuration +from custom_components.hacs.const import DOMAIN, STARTUP +from custom_components.hacs.enums import ConfigurationType, HacsStage, LovelaceMode from custom_components.hacs.hacsbase.data import HacsData -from custom_components.hacs.helpers.functions.constrains import check_constrains -from custom_components.hacs.helpers.functions.remaining_github_calls import ( - get_fetch_updates_for, -) -from custom_components.hacs.operational.reload import async_reload_entry -from custom_components.hacs.operational.remove import async_remove_entry -from custom_components.hacs.operational.setup_actions.clear_storage import ( - async_clear_storage, -) -from custom_components.hacs.operational.setup_actions.frontend import ( - async_setup_frontend, -) -from custom_components.hacs.operational.setup_actions.load_hacs_repository import ( - async_load_hacs_repository, -) -from custom_components.hacs.operational.setup_actions.sensor import async_add_sensor -from custom_components.hacs.operational.setup_actions.websocket_api import ( - async_setup_hacs_websockt_api, -) from custom_components.hacs.share import get_hacs +from custom_components.hacs.tasks.manager import HacsTaskManager try: from homeassistant.components.lovelace import system_health_info @@ -44,33 +21,82 @@ from homeassistant.components.lovelace.system_health import system_health_info -async def _async_common_setup(hass): +async def _async_common_setup(hass: HomeAssistant): """Common setup stages.""" + integration = await async_get_integration(hass, DOMAIN) + hacs = get_hacs() + + hacs.enable_hacs() + await hacs.async_set_stage(None) + + hacs.log.info(STARTUP.format(version=integration.version)) + + hacs.integration = integration + hacs.version = integration.version hacs.hass = hass + hacs.data = HacsData() hacs.system.running = True hacs.session = async_create_clientsession(hass) + hacs.tasks = HacsTaskManager(hacs=hacs, hass=hass) + + try: + lovelace_info = await system_health_info(hacs.hass) + except (TypeError, KeyError, HomeAssistantError): + # If this happens, the users YAML is not valid, we assume YAML mode + lovelace_info = {"mode": "yaml"} + hacs.log.debug(f"Configuration type: {hacs.configuration.config_type}") + hacs.core.config_path = hacs.hass.config.path() + hacs.core.ha_version = HAVERSION + hacs.core.lovelace_mode = lovelace_info.get("mode", "yaml") + hacs.core.lovelace_mode = LovelaceMode(lovelace_info.get("mode", "yaml")) -async def async_setup_entry(hass, config_entry): - """Set up this integration using UI.""" - from homeassistant import config_entries + await hacs.tasks.async_load() + + # Setup session for API clients + session = async_create_clientsession(hacs.hass) + ## Legacy GitHub client + hacs.github = GitHub( + hacs.configuration.token, + session, + headers={ + "User-Agent": f"HACS/{hacs.version}", + "Accept": ACCEPT_HEADERS["preview"], + }, + ) + + ## New GitHub client + hacs.githubapi = GitHubAPI( + token=hacs.configuration.token, + session=session, + **{"client_name": f"HACS/{hacs.version}"}, + ) + + hass.data[DOMAIN] = hacs + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" hacs = get_hacs() - if hass.data.get(DOMAIN) is not None: - return False - if config_entry.source == config_entries.SOURCE_IMPORT: + + if config_entry.source == SOURCE_IMPORT: hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) return False + if hass.data.get(DOMAIN) is not None: + return False - await _async_common_setup(hass) - - hacs.configuration = Configuration.from_dict( - config_entry.data, config_entry.options + hacs.configuration.update_from_dict( + { + "config_entry": config_entry, + "config_type": ConfigurationType.CONFIG_ENTRY, + **config_entry.data, + **config_entry.options, + } ) - hacs.configuration.config_type = "flow" - hacs.configuration.config_entry = config_entry + await _async_common_setup(hass) return await async_startup_wrapper_for_config_entry() @@ -79,13 +105,18 @@ async def async_setup(hass, config): hacs = get_hacs() if DOMAIN not in config: return True - if hacs.configuration and hacs.configuration.config_type == "flow": + if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY: return True - await _async_common_setup(hass) + hacs.configuration.update_from_dict( + { + "config_type": ConfigurationType.YAML, + **config[DOMAIN], + "config": config[DOMAIN], + } + ) - hacs.configuration = Configuration.from_dict(config[DOMAIN]) - hacs.configuration.config_type = "yaml" + await _async_common_setup(hass) await async_startup_wrapper_for_yaml() return True @@ -93,7 +124,7 @@ async def async_setup(hass, config): async def async_startup_wrapper_for_config_entry(): """Startup wrapper for ui config.""" hacs = get_hacs() - hacs.configuration.config_entry.add_update_listener(async_reload_entry) + try: startup_result = await async_hacs_startup() except AIOGitHubAPIException: @@ -101,7 +132,7 @@ async def async_startup_wrapper_for_config_entry(): if not startup_result: hacs.system.disabled = True raise ConfigEntryNotReady - hacs.enable() + hacs.enable_hacs() return startup_result @@ -117,86 +148,19 @@ async def async_startup_wrapper_for_yaml(_=None): hacs.log.info("Could not setup HACS, trying again in 15 min") async_call_later(hacs.hass, 900, async_startup_wrapper_for_yaml) return - hacs.enable() + hacs.enable_hacs() async def async_hacs_startup(): """HACS startup tasks.""" hacs = get_hacs() - hacs.hass.data[DOMAIN] = hacs - - try: - lovelace_info = await system_health_info(hacs.hass) - except (TypeError, HomeAssistantError): - # If this happens, the users YAML is not valid, we assume YAML mode - lovelace_info = {"mode": "yaml"} - hacs.log.debug(f"Configuration type: {hacs.configuration.config_type}") - hacs.version = INTEGRATION_VERSION - hacs.log.info(STARTUP) - hacs.core.config_path = hacs.hass.config.path() - hacs.system.ha_version = HAVERSION - - # Setup websocket API - await async_setup_hacs_websockt_api() - - # Set up frontend - await async_setup_frontend() - - # Clear old storage files - await async_clear_storage() - - hacs.system.lovelace_mode = lovelace_info.get("mode", "yaml") - hacs.enable() - hacs.github = GitHub( - hacs.configuration.token, - async_create_clientsession(hacs.hass), - headers=HACS_GITHUB_API_HEADERS, - ) - hacs.data = HacsData() - can_update = await get_fetch_updates_for(hacs.github) - if can_update is None: - hacs.log.critical("Your GitHub token is not valid") - hacs.disable(HacsDisabledReason.INVALID_TOKEN) + await hacs.async_set_stage(HacsStage.SETUP) + if hacs.system.disabled: return False - if can_update != 0: - hacs.log.debug(f"Can update {can_update} repositories") - else: - reset = datetime.fromtimestamp(int(hacs.github.client.ratelimits.reset)) - hacs.log.error( - "HACS is ratelimited, HACS will resume setup when the limit is cleared (%02d:%02d:%02d)", - reset.hour, - reset.minute, - reset.second, - ) - hacs.disable(HacsDisabledReason.RATE_LIMIT) - return False - - # Check HACS Constrains - if not await hacs.hass.async_add_executor_job(check_constrains): - if hacs.configuration.config_type == "flow": - if hacs.configuration.config_entry is not None: - await async_remove_entry(hacs.hass, hacs.configuration.config_entry) - hacs.disable(HacsDisabledReason.CONSTRAINS) - return False - - # Load HACS - if not await async_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) - hacs.disable(HacsDisabledReason.LOAD_HACS) - return False - - # Restore from storefiles - if not await hacs.data.restore(): - 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: - await async_remove_entry(hacs.hass, hacs.configuration.config_entry) - hacs.disable(HacsDisabledReason.RESTORE) + await hacs.async_set_stage(HacsStage.STARTUP) + if hacs.system.disabled: return False # Setup startup tasks @@ -205,12 +169,8 @@ async def async_hacs_startup(): else: hacs.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, hacs.startup_tasks) - # Set up sensor - await async_add_sensor() - # Mischief managed! await hacs.async_set_stage(HacsStage.WAITING) - hacs.log.info( - "Setup complete, waiting for Home Assistant before startup tasks starts" - ) - return True + hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts") + + return not hacs.system.disabled diff --git a/custom_components/hacs/operational/setup_actions/__init__.py b/custom_components/hacs/operational/setup_actions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/custom_components/hacs/operational/setup_actions/categories.py b/custom_components/hacs/operational/setup_actions/categories.py deleted file mode 100644 index 5092f66a..00000000 --- a/custom_components/hacs/operational/setup_actions/categories.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Starting setup task: extra stores.""" -from custom_components.hacs.const import ELEMENT_TYPES - -from ...enums import HacsCategory, HacsSetupTask -from ...share import get_hacs - - -def _setup_extra_stores(): - """Set up extra stores in HACS if enabled in Home Assistant.""" - hacs = get_hacs() - hacs.log.debug("Starting setup task: Extra stores") - hacs.common.categories = set() - for category in ELEMENT_TYPES: - enable_category(hacs, HacsCategory(category)) - - if HacsCategory.PYTHON_SCRIPT in hacs.hass.config.components: - enable_category(hacs, HacsCategory.PYTHON_SCRIPT) - - if ( - hacs.hass.services._services.get("frontend", {}).get("reload_themes") - is not None - ): - enable_category(hacs, HacsCategory.THEME) - - if hacs.configuration.appdaemon: - enable_category(hacs, HacsCategory.APPDAEMON) - if hacs.configuration.netdaemon: - enable_category(hacs, HacsCategory.NETDAEMON) - - -async def async_setup_extra_stores(): - """Async wrapper for setup_extra_stores""" - hacs = get_hacs() - hacs.log.info("setup task %s", HacsSetupTask.CATEGORIES) - await hacs.hass.async_add_executor_job(_setup_extra_stores) - - -def enable_category(hacs, category: HacsCategory): - """Add category.""" - if category not in hacs.common.categories: - hacs.log.info("Enable category: %s", category) - hacs.common.categories.add(category) diff --git a/custom_components/hacs/operational/setup_actions/clear_storage.py b/custom_components/hacs/operational/setup_actions/clear_storage.py deleted file mode 100644 index c67aaafa..00000000 --- a/custom_components/hacs/operational/setup_actions/clear_storage.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Starting setup task: clear storage.""" -import os - -from custom_components.hacs.share import get_hacs - -from ...enums import HacsSetupTask - - -async def async_clear_storage(): - """Async wrapper for clear_storage""" - hacs = get_hacs() - hacs.log.info("Setup task %s", HacsSetupTask.CATEGORIES) - await hacs.hass.async_add_executor_job(_clear_storage) - - -def _clear_storage(): - """Clear old files from storage.""" - hacs = get_hacs() - storagefiles = ["hacs"] - for s_f in storagefiles: - path = f"{hacs.core.config_path}/.storage/{s_f}" - if os.path.isfile(path): - hacs.log.info(f"Cleaning up old storage file {path}") - os.remove(path) diff --git a/custom_components/hacs/operational/setup_actions/frontend.py b/custom_components/hacs/operational/setup_actions/frontend.py deleted file mode 100644 index 1ad86936..00000000 --- a/custom_components/hacs/operational/setup_actions/frontend.py +++ /dev/null @@ -1,70 +0,0 @@ -from hacs_frontend.version import VERSION as FE_VERSION -from hacs_frontend import locate_dir - -from custom_components.hacs.helpers.functions.logger import getLogger -from custom_components.hacs.webresponses.frontend import HacsFrontendDev -from custom_components.hacs.helpers.functions.information import get_frontend_version -from custom_components.hacs.share import get_hacs - -from ...enums import HacsSetupTask - - -URL_BASE = "/hacsfiles" - - -async def async_setup_frontend(): - """Configure the HACS frontend elements.""" - hacs = get_hacs() - hacs.log.info("Setup task %s", HacsSetupTask.FRONTEND) - hass = hacs.hass - - # Register themes - hass.http.register_static_path(f"{URL_BASE}/themes", hass.config.path("themes")) - - # Register frontend - if hacs.configuration.frontend_repo_url: - getLogger().warning( - "Frontend development mode enabled. Do not run in production." - ) - hass.http.register_view(HacsFrontendDev()) - else: - # - hass.http.register_static_path( - f"{URL_BASE}/frontend", locate_dir(), cache_headers=False - ) - - # Custom iconset - hass.http.register_static_path( - f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js") - ) - if "frontend_extra_module_url" not in hass.data: - hass.data["frontend_extra_module_url"] = set() - hass.data["frontend_extra_module_url"].add("/hacsfiles/iconset.js") - - # Register www/community for all other files - hass.http.register_static_path( - URL_BASE, hass.config.path("www/community"), cache_headers=False - ) - - hacs.frontend.version_running = FE_VERSION - hacs.frontend.version_expected = await hass.async_add_executor_job( - get_frontend_version - ) - - # Add to sidepanel - if "hacs" not in hass.data.get("frontend_panels", {}): - hass.components.frontend.async_register_built_in_panel( - component_name="custom", - sidebar_title=hacs.configuration.sidepanel_title, - sidebar_icon=hacs.configuration.sidepanel_icon, - frontend_url_path="hacs", - config={ - "_panel_custom": { - "name": "hacs-frontend", - "embed_iframe": True, - "trust_external": False, - "js_url": "/hacsfiles/frontend/entrypoint.js", - } - }, - require_admin=True, - ) diff --git a/custom_components/hacs/operational/setup_actions/load_hacs_repository.py b/custom_components/hacs/operational/setup_actions/load_hacs_repository.py deleted file mode 100644 index 0cce8e9a..00000000 --- a/custom_components/hacs/operational/setup_actions/load_hacs_repository.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Starting setup task: load HACS repository.""" -from custom_components.hacs.const import INTEGRATION_VERSION -from custom_components.hacs.helpers.classes.exceptions import HacsException -from custom_components.hacs.helpers.functions.information import get_repository -from custom_components.hacs.helpers.functions.register_repository import ( - register_repository, -) -from custom_components.hacs.share import get_hacs - -from ...enums import HacsSetupTask - - -async def async_load_hacs_repository(): - """Load HACS repositroy.""" - hacs = get_hacs() - hacs.log.info("Setup task %s", HacsSetupTask.HACS_REPO) - - try: - repository = hacs.get_by_name("hacs/integration") - if repository is None: - await register_repository("hacs/integration", "integration") - repository = hacs.get_by_name("hacs/integration") - if repository is None: - raise HacsException("Unknown error") - repository.data.installed = True - repository.data.installed_version = INTEGRATION_VERSION - repository.data.new = False - hacs.repo = repository.repository_object - hacs.data_repo, _ = await get_repository( - hacs.session, hacs.configuration.token, "hacs/default", None - ) - except HacsException as exception: - if "403" in f"{exception}": - hacs.log.critical("GitHub API is ratelimited, or the token is wrong.") - else: - hacs.log.critical(f"[{exception}] - Could not load HACS!") - return False - return True diff --git a/custom_components/hacs/operational/setup_actions/sensor.py b/custom_components/hacs/operational/setup_actions/sensor.py deleted file mode 100644 index 13be265c..00000000 --- a/custom_components/hacs/operational/setup_actions/sensor.py +++ /dev/null @@ -1,25 +0,0 @@ -""""Starting setup task: Sensor".""" -from homeassistant.helpers import discovery - -from custom_components.hacs.const import DOMAIN -from custom_components.hacs.share import get_hacs - -from ...enums import HacsSetupTask - - -async def async_add_sensor(): - """Async wrapper for add sensor""" - hacs = get_hacs() - hacs.log.info("Setup task %s", HacsSetupTask.SENSOR) - if hacs.configuration.config_type == "yaml": - hacs.hass.async_create_task( - discovery.async_load_platform( - hacs.hass, "sensor", DOMAIN, {}, hacs.configuration.config - ) - ) - else: - hacs.hass.async_add_job( - hacs.hass.config_entries.async_forward_entry_setup( - hacs.configuration.config_entry, "sensor" - ) - ) diff --git a/custom_components/hacs/operational/setup_actions/websocket_api.py b/custom_components/hacs/operational/setup_actions/websocket_api.py deleted file mode 100644 index cf3e5b3a..00000000 --- a/custom_components/hacs/operational/setup_actions/websocket_api.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Register WS API endpoints for HACS.""" -from homeassistant.components import websocket_api - -from custom_components.hacs.api.acknowledge_critical_repository import ( - acknowledge_critical_repository, -) -from custom_components.hacs.api.check_local_path import check_local_path -from custom_components.hacs.api.get_critical_repositories import ( - get_critical_repositories, -) -from custom_components.hacs.api.hacs_config import hacs_config -from custom_components.hacs.api.hacs_removed import hacs_removed -from custom_components.hacs.api.hacs_repositories import hacs_repositories -from custom_components.hacs.api.hacs_repository import hacs_repository -from custom_components.hacs.api.hacs_repository_data import hacs_repository_data -from custom_components.hacs.api.hacs_settings import hacs_settings -from custom_components.hacs.api.hacs_status import hacs_status -from custom_components.hacs.share import get_hacs - -from ...enums import HacsSetupTask - - -async def async_setup_hacs_websockt_api(): - """Set up WS API handlers.""" - hacs = get_hacs() - hacs.log.info("Setup task %s", HacsSetupTask.WEBSOCKET) - websocket_api.async_register_command(hacs.hass, hacs_settings) - websocket_api.async_register_command(hacs.hass, hacs_config) - websocket_api.async_register_command(hacs.hass, hacs_repositories) - websocket_api.async_register_command(hacs.hass, hacs_repository) - websocket_api.async_register_command(hacs.hass, hacs_repository_data) - websocket_api.async_register_command(hacs.hass, check_local_path) - websocket_api.async_register_command(hacs.hass, hacs_status) - websocket_api.async_register_command(hacs.hass, hacs_removed) - websocket_api.async_register_command(hacs.hass, acknowledge_critical_repository) - websocket_api.async_register_command(hacs.hass, get_critical_repositories) diff --git a/custom_components/hacs/repositories/__init__.py b/custom_components/hacs/repositories/__init__.py index 91d89c42..d78def45 100644 --- a/custom_components/hacs/repositories/__init__.py +++ b/custom_components/hacs/repositories/__init__.py @@ -1,16 +1,16 @@ """Initialize repositories.""" -from custom_components.hacs.repositories.appdaemon import HacsAppdaemon -from custom_components.hacs.repositories.integration import HacsIntegration -from custom_components.hacs.repositories.netdaemon import HacsNetdaemon -from custom_components.hacs.repositories.plugin import HacsPlugin -from custom_components.hacs.repositories.python_script import HacsPythonScript -from custom_components.hacs.repositories.theme import HacsTheme +from custom_components.hacs.repositories.appdaemon import HacsAppdaemonRepository +from custom_components.hacs.repositories.integration import HacsIntegrationRepository +from custom_components.hacs.repositories.netdaemon import HacsNetdaemonRepository +from custom_components.hacs.repositories.plugin import HacsPluginRepository +from custom_components.hacs.repositories.python_script import HacsPythonScriptRepository +from custom_components.hacs.repositories.theme import HacsThemeRepository RERPOSITORY_CLASSES = { - "theme": HacsTheme, - "integration": HacsIntegration, - "python_script": HacsPythonScript, - "appdaemon": HacsAppdaemon, - "netdaemon": HacsNetdaemon, - "plugin": HacsPlugin, + "theme": HacsThemeRepository, + "integration": HacsIntegrationRepository, + "python_script": HacsPythonScriptRepository, + "appdaemon": HacsAppdaemonRepository, + "netdaemon": HacsNetdaemonRepository, + "plugin": HacsPluginRepository, } diff --git a/custom_components/hacs/repositories/appdaemon.py b/custom_components/hacs/repositories/appdaemon.py index c580b57c..a74186b0 100644 --- a/custom_components/hacs/repositories/appdaemon.py +++ b/custom_components/hacs/repositories/appdaemon.py @@ -2,11 +2,11 @@ from aiogithubapi import AIOGitHubAPIException from custom_components.hacs.enums import HacsCategory -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.classes.repository import HacsRepository -class HacsAppdaemon(HacsRepository): +class HacsAppdaemonRepository(HacsRepository): """Appdaemon apps in HACS.""" def __init__(self, full_name): @@ -61,9 +61,7 @@ async def update_repository(self, ignore_issues=False, force=False): self.content.path.remote = "" if self.content.path.remote == "apps": - addir = await self.repository_object.get_contents( - self.content.path.remote, self.ref - ) + addir = await self.repository_object.get_contents(self.content.path.remote, self.ref) self.content.path.remote = addir[0].path self.content.objects = await self.repository_object.get_contents( self.content.path.remote, self.ref diff --git a/custom_components/hacs/repositories/integration.py b/custom_components/hacs/repositories/integration.py index c73fe646..80758210 100644 --- a/custom_components/hacs/repositories/integration.py +++ b/custom_components/hacs/repositories/integration.py @@ -2,7 +2,7 @@ from homeassistant.loader import async_get_custom_components from custom_components.hacs.enums import HacsCategory -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.classes.repository import HacsRepository from custom_components.hacs.helpers.functions.filters import ( get_first_directory_in_directory, @@ -10,10 +10,9 @@ from custom_components.hacs.helpers.functions.information import ( get_integration_manifest, ) -from custom_components.hacs.helpers.functions.logger import getLogger -class HacsIntegration(HacsRepository): +class HacsIntegrationRepository(HacsRepository): """Integrations in HACS.""" def __init__(self, full_name): diff --git a/custom_components/hacs/repositories/netdaemon.py b/custom_components/hacs/repositories/netdaemon.py index ba4127f9..6796e71f 100644 --- a/custom_components/hacs/repositories/netdaemon.py +++ b/custom_components/hacs/repositories/netdaemon.py @@ -1,14 +1,13 @@ """Class for netdaemon apps in HACS.""" from custom_components.hacs.enums import HacsCategory -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.classes.repository import HacsRepository from custom_components.hacs.helpers.functions.filters import ( get_first_directory_in_directory, ) -from custom_components.hacs.helpers.functions.logger import getLogger -class HacsNetdaemon(HacsRepository): +class HacsNetdaemonRepository(HacsRepository): """Netdaemon apps in HACS.""" def __init__(self, full_name): @@ -35,16 +34,12 @@ async def validate_repository(self): 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.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" - ): + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".cs"): compliant = True break if not compliant: @@ -70,9 +65,7 @@ async def update_repository(self, ignore_issues=False, force=False): 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.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 diff --git a/custom_components/hacs/repositories/plugin.py b/custom_components/hacs/repositories/plugin.py index 8814eaac..1c0e026f 100644 --- a/custom_components/hacs/repositories/plugin.py +++ b/custom_components/hacs/repositories/plugin.py @@ -1,13 +1,12 @@ """Class for plugins in HACS.""" import json -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.classes.repository import HacsRepository from custom_components.hacs.helpers.functions.information import find_file_name -from custom_components.hacs.helpers.functions.logger import getLogger -class HacsPlugin(HacsRepository): +class HacsPluginRepository(HacsRepository): """Plugins in HACS.""" def __init__(self, full_name): @@ -67,9 +66,7 @@ async def update_repository(self, ignore_issues=False, force=False): async def get_package_content(self): """Get package content.""" try: - package = await self.repository_object.get_contents( - "package.json", self.ref - ) + package = await self.repository_object.get_contents("package.json", self.ref) package = json.loads(package.content) if package: diff --git a/custom_components/hacs/repositories/python_script.py b/custom_components/hacs/repositories/python_script.py index 00860b6e..50c9cfab 100644 --- a/custom_components/hacs/repositories/python_script.py +++ b/custom_components/hacs/repositories/python_script.py @@ -1,12 +1,11 @@ """Class for python_scripts in HACS.""" from custom_components.hacs.enums import HacsCategory -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.classes.repository import HacsRepository from custom_components.hacs.helpers.functions.information import find_file_name -from custom_components.hacs.helpers.functions.logger import getLogger -class HacsPythonScript(HacsRepository): +class HacsPythonScriptRepository(HacsRepository): """python_scripts in HACS.""" category = "python_script" @@ -37,9 +36,7 @@ async def validate_repository(self): compliant = False for treefile in self.treefiles: - if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( - ".py" - ): + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"): compliant = True break if not compliant: @@ -70,9 +67,7 @@ async def update_repository(self, ignore_issues=False, force=False): compliant = False for treefile in self.treefiles: - if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( - ".py" - ): + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"): compliant = True break if not compliant: diff --git a/custom_components/hacs/repositories/theme.py b/custom_components/hacs/repositories/theme.py index 4ce938ca..fcc61dbb 100644 --- a/custom_components/hacs/repositories/theme.py +++ b/custom_components/hacs/repositories/theme.py @@ -1,12 +1,11 @@ """Class for themes in HACS.""" from custom_components.hacs.enums import HacsCategory -from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.exceptions import HacsException from custom_components.hacs.helpers.classes.repository import HacsRepository from custom_components.hacs.helpers.functions.information import find_file_name -from custom_components.hacs.helpers.functions.logger import getLogger -class HacsTheme(HacsRepository): +class HacsThemeRepository(HacsRepository): """Themes in HACS.""" def __init__(self, full_name): diff --git a/custom_components/hacs/sensor.py b/custom_components/hacs/sensor.py index d5f6ceab..c7accc9f 100644 --- a/custom_components/hacs/sensor.py +++ b/custom_components/hacs/sensor.py @@ -2,13 +2,11 @@ from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from custom_components.hacs.const import DOMAIN, INTEGRATION_VERSION, NAME_SHORT -from custom_components.hacs.share import get_hacs +from custom_components.hacs.const import DOMAIN, NAME_SHORT +from custom_components.hacs.mixin import HacsMixin -async def async_setup_platform( - _hass, _config, async_add_entities, _discovery_info=None -): +async def async_setup_platform(_hass, _config, async_add_entities, _discovery_info=None): """Setup sensor platform.""" async_add_entities([HACSSensor()]) @@ -18,7 +16,7 @@ async def async_setup_entry(_hass, _config_entry, async_add_devices): async_add_devices([HACSSensor()]) -class HACSDevice(Entity): +class HACSDevice(HacsMixin, Entity): """HACS Device class.""" @property @@ -29,7 +27,7 @@ def device_info(self): "name": NAME_SHORT, "manufacturer": "hacs.xyz", "model": "", - "sw_version": INTEGRATION_VERSION, + "sw_version": str(self.hacs.version), "entry_type": "service", } @@ -60,16 +58,15 @@ def _update_and_write_state(self, *_): @callback def _update(self): """Update the sensor.""" - hacs = get_hacs() - if hacs.status.background_task: + if self.hacs.status.background_task: return self.repositories = [] - for repository in hacs.repositories: + for repository in self.hacs.repositories: if ( repository.pending_upgrade - and repository.data.category in hacs.common.categories + and repository.data.category in self.hacs.common.categories ): self.repositories.append(repository) self._state = len(self.repositories) @@ -77,9 +74,7 @@ def _update(self): @property def unique_id(self): """Return a unique ID to use for this sensor.""" - return ( - "0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd" - ) + return "0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd" @property def name(self): diff --git a/custom_components/hacs/share.py b/custom_components/hacs/share.py index a3665c2b..29c19be8 100644 --- a/custom_components/hacs/share.py +++ b/custom_components/hacs/share.py @@ -56,9 +56,7 @@ def get_removed(repository): removed_repo.repository = repository SHARE["removed_repositories"].append(removed_repo) filter_repos = [ - x - for x in SHARE["removed_repositories"] - if x.repository.lower() == repository.lower() + x for x in SHARE["removed_repositories"] if x.repository.lower() == repository.lower() ] return filter_repos.pop() or None diff --git a/custom_components/hacs/system_health.py b/custom_components/hacs/system_health.py index 37c03d1d..1a7860a2 100644 --- a/custom_components/hacs/system_health.py +++ b/custom_components/hacs/system_health.py @@ -10,9 +10,7 @@ @callback -def async_register( - hass: HomeAssistant, register: system_health.SystemHealthRegistration -) -> None: +def async_register(hass: HomeAssistant, register: system_health.SystemHealthRegistration) -> None: """Register system health callbacks.""" register.domain = "Home Assistant Community Store" register.async_register_info(system_health_info, "/hacs") @@ -20,18 +18,19 @@ def async_register( async def system_health_info(hass): """Get info for the info page.""" - client: HacsBase = hass.data[DOMAIN] - rate_limit = await client.github.get_rate_limit() - - return { - "GitHub API": system_health.async_check_can_reach_url( - hass, BASE_API_URL, GITHUB_STATUS - ), - "Github API Calls Remaining": rate_limit.get("remaining", "0"), - "Installed Version": client.version, - "Stage": client.stage, - "Available Repositories": len(client.repositories), - "Installed Repositories": len( - [repo for repo in client.repositories if repo.data.installed] - ), + hacs: HacsBase = hass.data[DOMAIN] + response = await hacs.githubapi.rate_limit() + + data = { + "GitHub API": system_health.async_check_can_reach_url(hass, BASE_API_URL, GITHUB_STATUS), + "Github API Calls Remaining": response.data.resources.core.remaining, + "Installed Version": hacs.version, + "Stage": hacs.stage, + "Available Repositories": len(hacs.repositories), + "Installed Repositories": len([repo for repo in hacs.repositories if repo.data.installed]), } + + if hacs.system.disabled: + data["Disabled"] = hacs.system.disabled_reason + + return data diff --git a/custom_components/hacs/tasks/__init__.py b/custom_components/hacs/tasks/__init__.py new file mode 100644 index 00000000..db567c4a --- /dev/null +++ b/custom_components/hacs/tasks/__init__.py @@ -0,0 +1 @@ +"""Init HACS tasks.""" diff --git a/custom_components/hacs/tasks/activate_categories.py b/custom_components/hacs/tasks/activate_categories.py new file mode 100644 index 00000000..07a6bbec --- /dev/null +++ b/custom_components/hacs/tasks/activate_categories.py @@ -0,0 +1,35 @@ +"""Starting setup task: extra stores.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsCategory, HacsStage +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Set up extra stores in HACS if enabled in Home Assistant.""" + + stages = [HacsStage.SETUP] + + def execute(self) -> None: + self.hacs.common.categories = set() + for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN): + self.hacs.enable_hacs_category(HacsCategory(category)) + + if HacsCategory.PYTHON_SCRIPT in self.hacs.hass.config.components: + self.hacs.enable_hacs_category(HacsCategory.PYTHON_SCRIPT) + + if self.hacs.hass.services.has_service("frontend", "reload_themes"): + self.hacs.enable_hacs_category(HacsCategory.THEME) + + if self.hacs.configuration.appdaemon: + self.hacs.enable_hacs_category(HacsCategory.APPDAEMON) + if self.hacs.configuration.netdaemon: + self.hacs.enable_hacs_category(HacsCategory.NETDAEMON) diff --git a/custom_components/hacs/tasks/base.py b/custom_components/hacs/tasks/base.py new file mode 100644 index 00000000..fc7d9b17 --- /dev/null +++ b/custom_components/hacs/tasks/base.py @@ -0,0 +1,58 @@ +""""Hacs base setup task.""" +# pylint: disable=abstract-method +from __future__ import annotations + +from datetime import timedelta +from timeit import default_timer as timer + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsStage +from ..mixin import LogMixin + + +class HacsTask(LogMixin): + """Hacs task base.""" + + hass: HomeAssistant + + events: list[str] | None = None + schedule: timedelta | None = None + stages: list[HacsStage] | None = None + + def __init__(self, hacs: HacsBase, hass: HomeAssistant) -> None: + self.hacs = hacs + self.hass = hass + + @property + def slug(self) -> str: + """Return the check slug.""" + return self.__class__.__module__.rsplit(".", maxsplit=1)[-1] + + async def execute_task(self, *_, **__) -> None: + """Execute the task defined in subclass.""" + if self.hacs.system.disabled: + self.log.warning( + "Skipping task %s, HACS is disabled - %s", + self.slug, + self.hacs.system.disabled_reason, + ) + return + self.log.info("Executing task: %s", self.slug) + start_time = timer() + + try: + if task := getattr(self, "execute", None): + await self.hass.async_add_executor_job(task) + elif task := getattr(self, "async_execute", None): + await task() # pylint: disable=not-callable + except BaseException as exception: # pylint: disable=broad-except + self.log.error("Task %s failed: %s", self.slug, exception) + + else: + self.log.debug( + "Task %s took " "%.2f seconds to complete", + self.slug, + timer() - start_time, + ) diff --git a/custom_components/hacs/tasks/check_constrains.py b/custom_components/hacs/tasks/check_constrains.py new file mode 100644 index 00000000..aca0fc17 --- /dev/null +++ b/custom_components/hacs/tasks/check_constrains.py @@ -0,0 +1,43 @@ +""""Starting setup task: Constrains".""" +from __future__ import annotations + +import os + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..const import MINIMUM_HA_VERSION +from ..enums import HacsDisabledReason, HacsStage +from ..utils.version import version_left_higher_then_right +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Check env Constrains.""" + + stages = [HacsStage.SETUP] + + def execute(self) -> None: + for location in ( + self.hass.config.path("custom_components/custom_updater.py"), + self.hass.config.path("custom_components/custom_updater/__init__.py"), + ): + if os.path.exists(location): + self.log.critical( + "This cannot be used with custom_updater. " + "To use this you need to remove custom_updater form %s", + location, + ) + self.hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) + + if not version_left_higher_then_right(self.hacs.core.ha_version, MINIMUM_HA_VERSION): + self.log.critical( + "You need HA version %s or newer to use this integration.", + MINIMUM_HA_VERSION, + ) + self.hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) diff --git a/custom_components/hacs/tasks/clear_old_storage.py b/custom_components/hacs/tasks/clear_old_storage.py new file mode 100644 index 00000000..20250cb8 --- /dev/null +++ b/custom_components/hacs/tasks/clear_old_storage.py @@ -0,0 +1,28 @@ +"""Starting setup task: clear storage.""" +from __future__ import annotations + +import os + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsStage +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Clear old files from storage.""" + + stages = [HacsStage.SETUP] + + def execute(self) -> None: + for storage_file in ("hacs",): + path = f"{self.hacs.core.config_path}/.storage/{storage_file}" + if os.path.isfile(path): + self.log.info("Cleaning up old storage file: %s", path) + os.remove(path) diff --git a/custom_components/hacs/tasks/hello_world.py b/custom_components/hacs/tasks/hello_world.py new file mode 100644 index 00000000..c4b9f720 --- /dev/null +++ b/custom_components/hacs/tasks/hello_world.py @@ -0,0 +1,23 @@ +""""Hacs base setup task.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """ "Hacs task base.""" + + schedule = timedelta(weeks=52) + + def execute(self) -> None: + self.log.debug("Hello World!") diff --git a/custom_components/hacs/tasks/load_hacs_repository.py b/custom_components/hacs/tasks/load_hacs_repository.py new file mode 100644 index 00000000..45956232 --- /dev/null +++ b/custom_components/hacs/tasks/load_hacs_repository.py @@ -0,0 +1,40 @@ +"""Starting setup task: load HACS repository.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsDisabledReason, HacsStage +from ..exceptions import HacsException +from ..helpers.functions.register_repository import register_repository +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Load HACS repositroy.""" + + stages = [HacsStage.STARTUP] + + async def async_execute(self) -> None: + try: + repository = self.hacs.get_by_name("hacs/integration") + if repository is None: + await register_repository("hacs/integration", "integration") + repository = self.hacs.get_by_name("hacs/integration") + if repository is None: + raise HacsException("Unknown error") + repository.data.installed = True + repository.data.installed_version = self.hacs.integration.version + repository.data.new = False + self.hacs.repository = repository.repository_object + except HacsException as exception: + if "403" in f"{exception}": + self.log.critical("GitHub API is ratelimited, or the token is wrong.") + else: + self.log.critical("[%s] - Could not load HACS!", exception) + self.hacs.disable_hacs(HacsDisabledReason.LOAD_HACS) diff --git a/custom_components/hacs/tasks/manager.py b/custom_components/hacs/tasks/manager.py new file mode 100644 index 00000000..3f17fa9b --- /dev/null +++ b/custom_components/hacs/tasks/manager.py @@ -0,0 +1,75 @@ +"""Hacs task manager.""" +from __future__ import annotations + +import asyncio +from importlib import import_module +from pathlib import Path + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..mixin import LogMixin +from .base import HacsTask + + +class HacsTaskManager(LogMixin): + """Hacs task manager.""" + + def __init__(self, hacs: HacsBase, hass: HomeAssistant) -> None: + """Initialize the setup manager class.""" + self.hacs = hacs + self.hass = hass + self.__tasks: dict[str, HacsTask] = {} + + @property + def tasks(self) -> list[HacsTask]: + """Return all list of all tasks.""" + return list(self.__tasks.values()) + + async def async_load(self) -> None: + """Load all tasks.""" + task_files = Path(__file__).parent + task_modules = ( + module.stem + for module in task_files.glob("*.py") + if module.name not in ("base.py", "__init__.py", "manager.py") + ) + + async def _load_module(module: str): + task_module = import_module(f"{__package__}.{module}") + if task := await task_module.async_setup_task(hacs=self.hacs, hass=self.hass): + self.__tasks[task.slug] = task + + await asyncio.gather(*[_load_module(task) for task in task_modules]) + self.log.info("Loaded %s tasks", len(self.tasks)) + + schedule_tasks = len(self.hacs.recuring_tasks) == 0 + + for task in self.tasks: + if task.events is not None: + for event in task.events: + self.hass.bus.async_listen_once(event, task.execute_task) + + if task.schedule is not None and schedule_tasks: + self.log.debug("Scheduling the %s task to run every %s", task.slug, task.schedule) + self.hacs.recuring_tasks.append( + self.hacs.hass.helpers.event.async_track_time_interval( + task.execute_task, task.schedule + ) + ) + + def get(self, slug: str) -> HacsTask | None: + """Return a task.""" + return self.__tasks.get(slug) + + async def async_execute_runtume_tasks(self) -> None: + """Execute the the execute methods of each runtime task if the stage matches.""" + self.hacs.status.background_task = True + await asyncio.gather( + *( + task.execute_task() + for task in self.tasks + if task.stages is not None and self.hacs.stage in task.stages + ) + ) + self.hacs.status.background_task = False diff --git a/custom_components/hacs/tasks/restore_data.py b/custom_components/hacs/tasks/restore_data.py new file mode 100644 index 00000000..e653d7c0 --- /dev/null +++ b/custom_components/hacs/tasks/restore_data.py @@ -0,0 +1,23 @@ +""""Starting setup task: Restore".""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsDisabledReason, HacsStage +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Restore HACS data.""" + + stages = [HacsStage.SETUP] + + async def async_execute(self) -> None: + if not await self.hacs.data.restore(): + self.hacs.disable_hacs(HacsDisabledReason.RESTORE) diff --git a/custom_components/hacs/tasks/setup_frontend.py b/custom_components/hacs/tasks/setup_frontend.py new file mode 100644 index 00000000..cbad9587 --- /dev/null +++ b/custom_components/hacs/tasks/setup_frontend.py @@ -0,0 +1,87 @@ +""""Starting setup task: Frontend".""" +from __future__ import annotations + +from hacs_frontend import locate_dir +from hacs_frontend.version import VERSION as FE_VERSION + +from ..const import DOMAIN +from ..enums import HacsStage +from ..webresponses.frontend import HacsFrontendDev +from .base import HacsTask + +URL_BASE = "/hacsfiles" + + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Setup the HACS frontend.""" + + stages = [HacsStage.SETUP] + + def execute(self) -> None: + + # Register themes + self.hass.http.register_static_path(f"{URL_BASE}/themes", self.hass.config.path("themes")) + + # Register frontend + if self.hacs.configuration.frontend_repo_url: + self.log.warning("Frontend development mode enabled. Do not run in production!") + self.hass.http.register_view(HacsFrontendDev()) + else: + # + self.hass.http.register_static_path( + f"{URL_BASE}/frontend", locate_dir(), cache_headers=False + ) + + # Custom iconset + self.hass.http.register_static_path( + f"{URL_BASE}/iconset.js", str(self.hacs.integration_dir / "iconset.js") + ) + if "frontend_extra_module_url" not in self.hass.data: + self.hass.data["frontend_extra_module_url"] = set() + self.hass.data["frontend_extra_module_url"].add("/hacsfiles/iconset.js") + + # Register www/community for all other files + use_cache = self.hacs.core.lovelace_mode == "storage" + self.log.info( + "%s mode, cache for /hacsfiles/: %s", + self.hacs.core.lovelace_mode, + use_cache, + ) + self.hass.http.register_static_path( + URL_BASE, + self.hass.config.path("www/community"), + cache_headers=use_cache, + ) + + self.hacs.frontend.version_running = FE_VERSION + for requirement in self.hacs.integration.requirements: + if "hacs_frontend" in requirement: + self.hacs.frontend.version_expected = requirement.split("==")[-1] + + # Add to sidepanel if needed + if DOMAIN not in self.hass.data.get("frontend_panels", {}): + self.hass.components.frontend.async_register_built_in_panel( + component_name="custom", + sidebar_title=self.hacs.configuration.sidepanel_title, + sidebar_icon=self.hacs.configuration.sidepanel_icon, + frontend_url_path=DOMAIN, + config={ + "_panel_custom": { + "name": "hacs-frontend", + "embed_iframe": True, + "trust_external": False, + "js_url": f"/hacsfiles/frontend/entrypoint.js?hacstag={FE_VERSION}", + } + }, + require_admin=True, + ) diff --git a/custom_components/hacs/tasks/setup_sensor.py b/custom_components/hacs/tasks/setup_sensor.py new file mode 100644 index 00000000..fa43bb38 --- /dev/null +++ b/custom_components/hacs/tasks/setup_sensor.py @@ -0,0 +1,31 @@ +""""Starting setup task: Sensor".""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform + +from ..base import HacsBase +from ..const import DOMAIN, PLATFORMS +from ..enums import ConfigurationType, HacsStage +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Setup the HACS sensor platform.""" + + stages = [HacsStage.SETUP] + + async def async_execute(self) -> None: + if self.hacs.configuration.config_type == ConfigurationType.YAML: + self.hass.async_create_task( + async_load_platform(self.hass, "sensor", DOMAIN, {}, self.hacs.configuration.config) + ) + else: + self.hass.config_entries.async_setup_platforms( + self.hacs.configuration.config_entry, PLATFORMS + ) diff --git a/custom_components/hacs/tasks/setup_websocket_api.py b/custom_components/hacs/tasks/setup_websocket_api.py new file mode 100644 index 00000000..f776c10e --- /dev/null +++ b/custom_components/hacs/tasks/setup_websocket_api.py @@ -0,0 +1,42 @@ +"""Register WS API endpoints for HACS.""" +from __future__ import annotations + +from homeassistant.components.websocket_api import async_register_command +from homeassistant.core import HomeAssistant + +from ..api.acknowledge_critical_repository import acknowledge_critical_repository +from ..api.check_local_path import check_local_path +from ..api.get_critical_repositories import get_critical_repositories +from ..api.hacs_config import hacs_config +from ..api.hacs_removed import hacs_removed +from ..api.hacs_repositories import hacs_repositories +from ..api.hacs_repository import hacs_repository +from ..api.hacs_repository_data import hacs_repository_data +from ..api.hacs_settings import hacs_settings +from ..api.hacs_status import hacs_status +from ..base import HacsBase +from ..enums import HacsStage +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Setup the HACS websocket API.""" + + stages = [HacsStage.SETUP] + + async def async_execute(self) -> None: + async_register_command(self.hass, hacs_settings) + async_register_command(self.hass, hacs_config) + async_register_command(self.hass, hacs_repositories) + async_register_command(self.hass, hacs_repository) + async_register_command(self.hass, hacs_repository_data) + async_register_command(self.hass, check_local_path) + async_register_command(self.hass, hacs_status) + async_register_command(self.hass, hacs_removed) + async_register_command(self.hass, acknowledge_critical_repository) + async_register_command(self.hass, get_critical_repositories) diff --git a/custom_components/hacs/tasks/store_hacs_data.py b/custom_components/hacs/tasks/store_hacs_data.py new file mode 100644 index 00000000..ccabd4a5 --- /dev/null +++ b/custom_components/hacs/tasks/store_hacs_data.py @@ -0,0 +1,22 @@ +""""Store HACS data.""" +from __future__ import annotations + +from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """ "Hacs task base.""" + + events = [EVENT_HOMEASSISTANT_FINAL_WRITE] + + async def async_execute(self) -> None: + await self.hacs.data.async_write() diff --git a/custom_components/hacs/tasks/verify_api.py b/custom_components/hacs/tasks/verify_api.py new file mode 100644 index 00000000..02b2ae35 --- /dev/null +++ b/custom_components/hacs/tasks/verify_api.py @@ -0,0 +1,23 @@ +""""Starting setup task: Verify API".""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsStage +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Verify the connection to the GitHub API.""" + + stages = [HacsStage.SETUP] + + async def async_execute(self) -> None: + can_update = await self.hacs.async_can_update() + self.log.debug("Can update %s repositories", can_update) diff --git a/custom_components/hacs/translations/en.json b/custom_components/hacs/translations/en.json index 7ed7b446..12c6c07b 100644 --- a/custom_components/hacs/translations/en.json +++ b/custom_components/hacs/translations/en.json @@ -30,6 +30,10 @@ } }, "options": { + "abort": { + "not_setup": "HACS is not setup.", + "release_limit_value": "The release limit needs to be between 1 and 100" + }, "step": { "user": { "data": { diff --git a/custom_components/hacs/utils/__init__.py b/custom_components/hacs/utils/__init__.py new file mode 100644 index 00000000..58b214ca --- /dev/null +++ b/custom_components/hacs/utils/__init__.py @@ -0,0 +1 @@ +"""Initialize HACS utils.""" diff --git a/custom_components/hacs/utils/decode.py b/custom_components/hacs/utils/decode.py new file mode 100644 index 00000000..9d42516c --- /dev/null +++ b/custom_components/hacs/utils/decode.py @@ -0,0 +1,7 @@ +"""Util to decode content from the github API.""" +from base64 import b64decode + + +def decode_content(content: str) -> str: + """Decode content.""" + return b64decode(bytearray(content, "utf-8")).decode() diff --git a/custom_components/hacs/helpers/functions/logger.py b/custom_components/hacs/utils/logger.py similarity index 92% rename from custom_components/hacs/helpers/functions/logger.py rename to custom_components/hacs/utils/logger.py index 900bc916..c3fc4dd7 100644 --- a/custom_components/hacs/helpers/functions/logger.py +++ b/custom_components/hacs/utils/logger.py @@ -3,7 +3,7 @@ import logging import os -from ...const import PACKAGE_NAME +from ..const import PACKAGE_NAME _HACSLogger: logging.Logger = logging.getLogger(PACKAGE_NAME) diff --git a/custom_components/hacs/utils/path.py b/custom_components/hacs/utils/path.py new file mode 100644 index 00000000..3e10e47c --- /dev/null +++ b/custom_components/hacs/utils/path.py @@ -0,0 +1,21 @@ +"""Path utils""" +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..hacsbase.hacs import Hacs + + +def is_safe(hacs: Hacs, path: str | Path) -> bool: + """Helper to check if path is safe to remove.""" + paths = [ + Path(f"{hacs.core.config_path}/{hacs.configuration.appdaemon_path}"), + Path(f"{hacs.core.config_path}/{hacs.configuration.netdaemon_path}"), + Path(f"{hacs.core.config_path}/{hacs.configuration.plugin_path}"), + Path(f"{hacs.core.config_path}/{hacs.configuration.python_script_path}"), + Path(f"{hacs.core.config_path}/{hacs.configuration.theme_path}"), + Path(f"{hacs.core.config_path}/custom_components/"), + ] + return Path(path) not in paths diff --git a/custom_components/hacs/utils/version.py b/custom_components/hacs/utils/version.py new file mode 100644 index 00000000..77f28fa5 --- /dev/null +++ b/custom_components/hacs/utils/version.py @@ -0,0 +1,15 @@ +"""Version utils.""" + + +from functools import lru_cache + +from awesomeversion import AwesomeVersionException, AwesomeVersion + + +@lru_cache(maxsize=1024) +def version_left_higher_then_right(left: str, right: str) -> bool: + """Return a bool if source is newer than target, will also be true if identical.""" + try: + return AwesomeVersion(left) >= AwesomeVersion(right) + except AwesomeVersionException: + return False diff --git a/custom_components/hacs/webresponses/frontend.py b/custom_components/hacs/webresponses/frontend.py index b1a32f9b..8b29c9b0 100644 --- a/custom_components/hacs/webresponses/frontend.py +++ b/custom_components/hacs/webresponses/frontend.py @@ -1,6 +1,6 @@ from aiohttp import web - from homeassistant.components.http import HomeAssistantView + from custom_components.hacs.share import get_hacs @@ -15,9 +15,7 @@ async def get(self, request, requested_file): # pylint: disable=unused-argument """Handle HACS Web requests.""" hacs = get_hacs() requested = requested_file.split("/")[-1] - request = await hacs.session.get( - f"{hacs.configuration.frontend_repo_url}/{requested}" - ) + request = await hacs.session.get(f"{hacs.configuration.frontend_repo_url}/{requested}") if request.status == 200: result = await request.read() response = web.Response(body=result) diff --git a/custom_components/here_weather/__init__.py b/custom_components/here_weather/__init__.py new file mode 100644 index 00000000..8ecd8b9f --- /dev/null +++ b/custom_components/here_weather/__init__.py @@ -0,0 +1,111 @@ +"""The here_weather component.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +import aiohere +import async_timeout +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_MODES, DEFAULT_SCAN_INTERVAL, DOMAIN, STARTUP_MESSAGE + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor", "weather"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up here_weather from a config entry.""" + if hass.data.get(DOMAIN) is None: + _LOGGER.info(STARTUP_MESSAGE) + here_weather_coordinators = {} + for mode in CONF_MODES: + coordinator = HEREWeatherDataUpdateCoordinator(hass, entry, mode) + await coordinator.async_config_entry_first_refresh() + here_weather_coordinators[mode] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = here_weather_coordinators + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class HEREWeatherDataUpdateCoordinator(DataUpdateCoordinator): + """Get the latest data from HERE.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, mode: str) -> None: + """Initialize the data object.""" + session = async_get_clientsession(hass) + self.here_client = aiohere.AioHere(entry.data[CONF_API_KEY], session=session) + self.latitude = entry.data[CONF_LATITUDE] + self.longitude = entry.data[CONF_LONGITUDE] + self.weather_product_type = aiohere.WeatherProductType[mode] + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + + async def _async_update_data(self) -> list: + """Perform data update.""" + try: + async with async_timeout.timeout(10): + data = await self._get_data() + return data + except aiohere.HereError as error: + raise UpdateFailed( + f"Unable to fetch data from HERE: {error.args[0]}" + ) from error + + async def _get_data(self): + """Get the latest data from HERE.""" + is_metric = self.hass.config.units.name == CONF_UNIT_SYSTEM_METRIC + data = await self.here_client.weather_for_coordinates( + self.latitude, + self.longitude, + self.weather_product_type, + metric=is_metric, + ) + return extract_data_from_payload_for_product_type( + data, self.weather_product_type + ) + + +def extract_data_from_payload_for_product_type( + data: dict[str, Any], product_type: aiohere.WeatherProductType +) -> list: + """Extract the actual data from the HERE payload.""" + if product_type == aiohere.WeatherProductType.FORECAST_ASTRONOMY: + return data["astronomy"]["astronomy"] + if product_type == aiohere.WeatherProductType.OBSERVATION: + return data["observations"]["location"][0]["observation"] + if product_type == aiohere.WeatherProductType.FORECAST_7DAYS: + return data["forecasts"]["forecastLocation"]["forecast"] + if product_type == aiohere.WeatherProductType.FORECAST_7DAYS_SIMPLE: + return data["dailyForecasts"]["forecastLocation"]["forecast"] + if product_type == aiohere.WeatherProductType.FORECAST_HOURLY: + return data["hourlyForecasts"]["forecastLocation"]["forecast"] + _LOGGER.debug("Payload malformed: %s", data) + raise UpdateFailed("Payload malformed") diff --git a/custom_components/here_weather/config_flow.py b/custom_components/here_weather/config_flow.py new file mode 100644 index 00000000..7a00d2e0 --- /dev/null +++ b/custom_components/here_weather/config_flow.py @@ -0,0 +1,82 @@ +"""Config flow for here_weather integration.""" +from __future__ import annotations + +import aiohere +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +from .const import DEFAULT_MODE, DOMAIN + + +async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> None: + """Validate the user_input containing coordinates.""" + session = async_get_clientsession(hass) + here_client = aiohere.AioHere(user_input[CONF_API_KEY], session=session) + await here_client.weather_for_coordinates( + user_input[CONF_LATITUDE], + user_input[CONF_LONGITUDE], + aiohere.WeatherProductType[DEFAULT_MODE], + ) + + +class HereWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for here_weather.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id(_unique_id(user_input)) + self._abort_if_unique_id_configured() + try: + await async_validate_user_input(self.hass, user_input) + except aiohere.HereInvalidRequestError: + errors["base"] = "invalid_request" + except aiohere.HereUnauthorizedError: + errors["base"] = "unauthorized" + else: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + return self.async_show_form( + step_id="user", + data_schema=self._get_schema(user_input), + errors=errors, + ) + + def _get_schema(self, user_input: dict | None) -> vol.Schema: + if user_input is not None: + return vol.Schema( + { + vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str, + vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str, + vol.Required( + CONF_LATITUDE, default=user_input[CONF_LATITUDE] + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=user_input[CONF_LONGITUDE] + ): cv.longitude, + } + ) + return vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_NAME, default=DOMAIN): str, + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ) + + +def _unique_id(user_input: dict) -> str: + return f"{user_input[CONF_LATITUDE]}_{user_input[CONF_LONGITUDE]}" diff --git a/custom_components/here_weather/const.py b/custom_components/here_weather/const.py new file mode 100644 index 00000000..013372fd --- /dev/null +++ b/custom_components/here_weather/const.py @@ -0,0 +1,513 @@ +"""Constants for the HERE Destination Weather service.""" +from __future__ import annotations + +from homeassistant.const import ( + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + LENGTH_CENTIMETERS, + LENGTH_KILOMETERS, + PERCENTAGE, + PRESSURE_MBAR, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) + +NAME = "HERE Destination Weather" +DOMAIN = "here_weather" +VERSION = "1.0.2" +ISSUE_URL = "https://github.com/eifinger/hass-here-weather/issues" + +STARTUP_MESSAGE = f""" +------------------------------------------------------------------- +{NAME} +Version: {VERSION} +This is a custom integration! +If you have any issues with this you need to open an issue here: +{ISSUE_URL} +------------------------------------------------------------------- +""" + +DEFAULT_SCAN_INTERVAL = 1800 # 30 minutes + +MODE_ASTRONOMY = "FORECAST_ASTRONOMY" +MODE_HOURLY = "FORECAST_HOURLY" +MODE_DAILY = "FORECAST_7DAYS" +MODE_DAILY_SIMPLE = "FORECAST_7DAYS_SIMPLE" +MODE_OBSERVATION = "OBSERVATION" +CONF_MODES = [ + MODE_ASTRONOMY, + MODE_HOURLY, + MODE_DAILY, + MODE_DAILY_SIMPLE, + MODE_OBSERVATION, +] +DEFAULT_MODE = MODE_DAILY_SIMPLE + +ASTRONOMY_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "sunrise": {"name": "Sunrise", "unit_of_measurement": None, "device_class": None}, + "sunset": {"name": "Sunset", "unit_of_measurement": None, "device_class": None}, + "moonrise": {"name": "Moonrise", "unit_of_measurement": None, "device_class": None}, + "moonset": {"name": "Moonset", "unit_of_measurement": None, "device_class": None}, + "moonPhase": { + "name": "Moon Phase", + "unit_of_measurement": PERCENTAGE, + "device_class": None, + }, + "moonPhaseDesc": { + "name": "Moon Phase Description", + "unit_of_measurement": None, + "device_class": None, + }, + "city": {"name": "City", "unit_of_measurement": None, "device_class": None}, + "latitude": {"name": "Latitude", "unit_of_measurement": None, "device_class": None}, + "longitude": { + "name": "Longitude", + "unit_of_measurement": None, + "device_class": None, + }, + "utcTime": { + "name": "UTC Time", + "unit_of_measurement": None, + "device_class": DEVICE_CLASS_TIMESTAMP, + }, +} + +COMMON_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "daylight": {"name": "Daylight", "unit_of_measurement": None, "device_class": None}, + "description": { + "name": "Description", + "unit_of_measurement": None, + "device_class": None, + }, + "skyInfo": {"name": "Sky Info", "unit_of_measurement": None, "device_class": None}, + "skyDescription": { + "name": "Sky Description", + "unit_of_measurement": None, + "device_class": None, + }, + "temperatureDesc": { + "name": "Temperature Description", + "unit_of_measurement": None, + "device_class": None, + }, + "comfort": { + "name": "Comfort", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "humidity": { + "name": "Humidity", + "unit_of_measurement": PERCENTAGE, + "device_class": DEVICE_CLASS_HUMIDITY, + }, + "dewPoint": { + "name": "Dew Point", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "precipitationProbability": { + "name": "Precipitation Probability", + "unit_of_measurement": PERCENTAGE, + "device_class": None, + }, + "precipitationDesc": { + "name": "Precipitation Description", + "unit_of_measurement": None, + "device_class": None, + }, + "airDescription": { + "name": "Air Description", + "unit_of_measurement": None, + "device_class": None, + }, + "windSpeed": { + "name": "Wind Speed", + "unit_of_measurement": SPEED_KILOMETERS_PER_HOUR, + "device_class": None, + }, + "windDirection": { + "name": "Wind Direction", + "unit_of_measurement": DEGREE, + "device_class": None, + }, + "windDesc": { + "name": "Wind Description", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "windDescShort": { + "name": "Wind Description Short", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "icon": {"name": "Icon", "unit_of_measurement": None, "device_class": None}, + "iconName": { + "name": "Icon Name", + "unit_of_measurement": None, + "device_class": None, + }, + "iconLink": { + "name": "Icon Link", + "unit_of_measurement": None, + "device_class": None, + }, +} + +NON_OBSERVATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "rainFall": { + "name": "Rain Fall", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "snowFall": { + "name": "Snow Fall", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "precipitationProbability": { + "name": "Precipitation Probability", + "unit_of_measurement": PERCENTAGE, + "device_class": None, + }, + "dayOfWeek": { + "name": "Day of Week", + "unit_of_measurement": None, + "device_class": None, + }, + "weekday": {"name": "Week Day", "unit_of_measurement": None, "device_class": None}, + "utcTime": { + "name": "UTC Time", + "unit_of_measurement": None, + "device_class": DEVICE_CLASS_TIMESTAMP, + }, + **COMMON_ATTRIBUTES, +} + +HOURLY_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "temperature": { + "name": "Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "airInfo": {"name": "Air Info", "unit_of_measurement": None, "device_class": None}, + "visibility": { + "name": "Visibility", + "unit_of_measurement": LENGTH_KILOMETERS, + "device_class": None, + }, + **NON_OBSERVATION_ATTRIBUTES, +} + +DAILY_SIMPLE_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "highTemperature": { + "name": "High Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "lowTemperature": { + "name": "Low Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "beaufortScale": { + "name": "Beaufort Scale", + "unit_of_measurement": None, + "device_class": None, + }, + "beaufortDescription": { + "name": "Beaufort Scale Description", + "unit_of_measurement": None, + "device_class": None, + }, + "uvIndex": {"name": "UV Index", "unit_of_measurement": None, "device_class": None}, + "uvDesc": { + "name": "UV Index Description", + "unit_of_measurement": None, + "device_class": None, + }, + "barometerPressure": { + "name": "Barometric Pressure", + "unit_of_measurement": PRESSURE_MBAR, + "device_class": DEVICE_CLASS_PRESSURE, + }, + **NON_OBSERVATION_ATTRIBUTES, +} + +DAILY_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "daySegment": { + "name": "Day Segment", + "unit_of_measurement": None, + "device_class": None, + }, + "temperature": { + "name": "Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "beaufortScale": { + "name": "Beaufort Scale", + "unit_of_measurement": None, + "device_class": None, + }, + "beaufortDescription": { + "name": "Beaufort Scale Description", + "unit_of_measurement": None, + "device_class": None, + }, + "visibility": { + "name": "Visibility", + "unit_of_measurement": LENGTH_KILOMETERS, + "device_class": None, + }, + **NON_OBSERVATION_ATTRIBUTES, +} + +OBSERVATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "temperature": { + "name": "Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "highTemperature": { + "name": "High Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "lowTemperature": { + "name": "Low Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "precipitation1H": { + "name": "Precipitation Over 1 Hour", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "precipitation3H": { + "name": "Precipitation Over 3 Hours", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "precipitation6H": { + "name": "Precipitation Over 6 Hours", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "precipitation12H": { + "name": "Precipitation Over 12 Hours", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "precipitation24H": { + "name": "Precipitation Over 24 Hours", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "barometerPressure": { + "name": "Barometric Pressure", + "unit_of_measurement": PRESSURE_MBAR, + "device_class": DEVICE_CLASS_PRESSURE, + }, + "barometerTrend": { + "name": "Barometric Pressure Trend", + "unit_of_measurement": None, + "device_class": None, + }, + "visibility": { + "name": "Visibility", + "unit_of_measurement": LENGTH_KILOMETERS, + "device_class": None, + }, + "snowCover": { + "name": "Snow Cover", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "activeAlerts": { + "name": "Active Alerts", + "unit_of_measurement": None, + "device_class": None, + }, + "country": {"name": "Country", "unit_of_measurement": None, "device_class": None}, + "state": {"name": "State", "unit_of_measurement": None, "device_class": None}, + "city": {"name": "City", "unit_of_measurement": None, "device_class": None}, + **COMMON_ATTRIBUTES, +} + +SENSOR_TYPES: dict[str, dict[str, dict[str, str | None]]] = { + MODE_ASTRONOMY: ASTRONOMY_ATTRIBUTES, + MODE_HOURLY: HOURLY_ATTRIBUTES, + MODE_DAILY_SIMPLE: DAILY_SIMPLE_ATTRIBUTES, + MODE_DAILY: DAILY_ATTRIBUTES, + MODE_OBSERVATION: OBSERVATION_ATTRIBUTES, +} + +CONDITION_CLASSES: dict[str, list[str]] = { + "clear-night": [ + "night_passing_clouds", + "night_mostly_clear", + "night_clearing_skies", + "night_clear", + ], + "cloudy": [ + "night_decreasing_cloudiness", + "night_mostly_cloudy", + "night_morning_clouds", + "night_afternoon_clouds", + "night_high_clouds", + "night_high_level_clouds", + "low_clouds", + "overcast", + "cloudy", + "afternoon_clouds", + "morning_clouds", + "high_level_clouds", + "high_clouds", + ], + "fog": [ + "night_low_level_haze", + "night_smoke", + "night_haze", + "dense_fog", + "fog", + "light_fog", + "early_fog", + "early_fog_followed_by_sunny_skies", + "low_level_haze", + "smoke", + "haze", + "ice_fog", + "hazy_sunshine", + ], + "hail": ["hail"], + "lightning": [ + "night_tstorms", + "night_scattered_tstorms", + "scattered_tstorms", + "night_a_few_tstorms", + "night_isolated_tstorms", + "night_widely_scattered_tstorms", + "tstorms_early", + "isolated_tstorms_late", + "scattered_tstorms_late", + "widely_scattered_tstorms", + "isolated_tstorms", + "a_few_tstorms", + ], + "lightning-rainy": ["thundershowers", "thunderstorms", "tstorms_late", "tstorms"], + "partlycloudy": [ + "partly_sunny", + "mostly_cloudy", + "broken_clouds", + "more_clouds_than_sun", + "night_broken_clouds", + "increasing_cloudiness", + "night_partly_cloudy", + "night_scattered_clouds", + "passing_clounds", + "more_sun_than_clouds", + "scattered_clouds", + "partly_cloudy", + "a_mixture_of_sun_and_clouds", + "increasing_cloudiness", + "decreasing_cloudiness", + "clearing_skies", + "breaks_of_sun_late", + ], + "pouring": [ + "heavy_rain_late", + "heavy_rain_early", + "tons_of_rain", + "lots_of_rain", + "heavy_rain", + "heavy_rain_early", + "heavy_rain_early", + ], + "rainy": [ + "rain_late", + "showers_late", + "rain_early", + "showery", + "showers_early", + "numerous_showers", + "rain", + "light_rain_late", + "sprinkles_late", + "light_rain_early", + "sprinkles_early", + "light_rain", + "sprinkles", + "drizzle", + "night_showers", + "night_sprinkles", + "night_rain_showers", + "night_passing_showers", + "night_light_showers", + "night_a_few_showers", + "night_scattered_showers", + "rain_early", + "scattered_showers", + "a_few_showers", + "light_showers", + "passing_showers", + "rain_showers", + "showers", + ], + "snowy": [ + "heavy_snow_late", + "heavy_snow_early", + "heavy_snow", + "snow_late", + "snow_early", + "moderate_snow", + "snow", + "light_snow_late", + "snow_showers_late", + "flurries_late", + "light_snow_early", + "flurries_early", + "light_snow", + "snow_flurries", + "sleet", + "an_icy_mix_changing_to_snow", + "an_icy_mix_changing_to_rain", + "rain_changing_to_snow", + "icy_mix_early", + "light_icy_mix_late", + "icy_mix_late", + "scattered_flurries", + ], + "snowy-rainy": [ + "freezing_rain", + "light_freezing_rain", + "snow_showers_early", + "snow_showers", + "light_snow_showers", + "snow_rain_mix", + "light_icy_mix_early", + "rain_changing_to_an_icy_mix", + "snow_changing_to_an_icy_mix", + "snow_changing_to_rain", + "heavy_mixture_of_precip", + "light_mixture_of_precip", + "icy_mix", + "mixture_of_precip", + ], + "sunny": ["sunny", "clear", "mostly_sunny", "mostly_clear"], + "windy": ["strong_thunderstorms", "severe_thunderstorms"], + "windy-variant": [], + "exceptional": [ + "blizzard", + "snowstorm", + "duststorm", + "sandstorm", + "hurricane", + "tropical_storm", + "flood", + "flash_floods", + "tornado", + ], +} diff --git a/custom_components/here_weather/manifest.json b/custom_components/here_weather/manifest.json new file mode 100644 index 00000000..98bf0ac5 --- /dev/null +++ b/custom_components/here_weather/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "here_weather", + "name": "HERE Destination Weather", + "documentation": "https://github.com/eifinger/hass-here-weather", + "issue_tracker": "https://github.com/eifinger/hass-here-weather/issues", + "dependencies": [], + "version": "1.0.2", + "config_flow": true, + "codeowners": [ + "@eifinger" + ], + "requirements": [ + "aiohere==1.2.0" + ], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/custom_components/here_weather/sensor.py b/custom_components/here_weather/sensor.py new file mode 100644 index 00000000..62bbf241 --- /dev/null +++ b/custom_components/here_weather/sensor.py @@ -0,0 +1,116 @@ +"""Sensor platform for the HERE Destination Weather service.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, SENSOR_TYPES +from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Add here_weather entities from a ConfigEntry.""" + here_weather_coordinators = hass.data[DOMAIN][entry.entry_id] + + sensors_to_add = [] + for sensor_type, weather_attributes in SENSOR_TYPES.items(): + for weather_attribute in weather_attributes: + sensors_to_add.append( + HEREDestinationWeatherSensor( + entry, + here_weather_coordinators[sensor_type], + sensor_type, + weather_attribute, + ) + ) + async_add_entities(sensors_to_add) + + +class HEREDestinationWeatherSensor(CoordinatorEntity, SensorEntity): + """Implementation of an HERE Destination Weather sensor.""" + + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator, + sensor_type: str, + weather_attribute: str, + sensor_number: int = 0, # Additional supported offsets will be added in a separate PR + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._base_name = entry.data[CONF_NAME] + self._name_suffix = SENSOR_TYPES[sensor_type][weather_attribute]["name"] + self._latitude = entry.data[CONF_LATITUDE] + self._longitude = entry.data[CONF_LONGITUDE] + self._sensor_type = sensor_type + self._sensor_number = sensor_number + self._weather_attribute = weather_attribute + self._unit_of_measurement = convert_unit_of_measurement_if_needed( + self.coordinator.hass.config.units.name, + SENSOR_TYPES[sensor_type][weather_attribute]["unit_of_measurement"], + ) + self._device_class = SENSOR_TYPES[sensor_type][weather_attribute][ + "device_class" + ] + self._unique_id = "".join( + f"{self._latitude}_{self._longitude}_{self._sensor_type}_{self._name_suffix}_{self._sensor_number}".lower().split() + ) + self._unique_device_id = "".join( + f"{self._latitude}_{self._longitude}_{self._sensor_type}".lower().split() + ) + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"{self._base_name} {self._sensor_type} {self._name_suffix} {self._sensor_number}" + + @property + def unique_id(self) -> str: + """Set unique_id for sensor.""" + return self._unique_id + + @property + def state(self) -> StateType: + """Return the state of the device.""" + return get_attribute_from_here_data( + self.coordinator.data, + self._weather_attribute, + self._sensor_number, + ) + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_info(self) -> DeviceInfo: + """Return a device description for device registry.""" + + return { + "identifiers": {(DOMAIN, self._unique_device_id)}, + "name": f"{self._base_name} {self._sensor_type}", + "manufacturer": "here.com", + "entry_type": "service", + } + + @property + def device_class(self) -> str | None: + """Return the class of this device.""" + return self._device_class diff --git a/custom_components/here_weather/translations/de.json b/custom_components/here_weather/translations/de.json new file mode 100644 index 00000000..9648d1f9 --- /dev/null +++ b/custom_components/here_weather/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "description": "Möchtest Du mit der Einrichtung beginnen?", + "data": { + "api_key": "API-Schlücssel", + "latitude": "Breitengrad", + "longitude": "Längengrad", + "name": "Name" + } + } + }, + "error": { + "invalid_request": "HERE hat eine invalide Anfrage entdeckt. Das deutet daraufhin, dass die Koordination falsch sind.", + "unauthorized": "Ungültiger API Key." + } + } +} \ No newline at end of file diff --git a/custom_components/here_weather/translations/en.json b/custom_components/here_weather/translations/en.json new file mode 100644 index 00000000..926ef773 --- /dev/null +++ b/custom_components/here_weather/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to start set up?", + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + } + } + }, + "error": { + "invalid_request": "HERE reported an invalid request. This indicates the supplied location is not valid.", + "unauthorized": "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + } + } +} \ No newline at end of file diff --git a/custom_components/here_weather/utils.py b/custom_components/here_weather/utils.py new file mode 100644 index 00000000..13dc85eb --- /dev/null +++ b/custom_components/here_weather/utils.py @@ -0,0 +1,62 @@ +"""Utility functions for here_weather.""" +from __future__ import annotations + +from homeassistant.const import ( + CONF_UNIT_SYSTEM_METRIC, + LENGTH_CENTIMETERS, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + PRESSURE_INHG, + PRESSURE_MBAR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) + + +def convert_temperature_unit_of_measurement_if_needed( + unit_system: str, unit_of_measurement: str +) -> str: + """Convert the temperature unit of measurement to imperial if configured.""" + if unit_system != CONF_UNIT_SYSTEM_METRIC: + unit_of_measurement = TEMP_FAHRENHEIT + return unit_of_measurement + + +def convert_unit_of_measurement_if_needed( + unit_system: str, unit_of_measurement: str | None +) -> str | None: + """Convert the unit of measurement to imperial if configured.""" + if unit_system != CONF_UNIT_SYSTEM_METRIC: + if unit_of_measurement == TEMP_CELSIUS: + unit_of_measurement = TEMP_FAHRENHEIT + elif unit_of_measurement == LENGTH_CENTIMETERS: + unit_of_measurement = LENGTH_INCHES + elif unit_of_measurement == SPEED_KILOMETERS_PER_HOUR: + unit_of_measurement = SPEED_MILES_PER_HOUR + elif unit_of_measurement == PRESSURE_MBAR: + unit_of_measurement = PRESSURE_INHG + elif unit_of_measurement == LENGTH_KILOMETERS: + unit_of_measurement = LENGTH_MILES + return unit_of_measurement + + +def get_attribute_from_here_data( + here_data: list, attribute_name: str, sensor_number: int = 0 +) -> str | None: + """Extract and convert data from HERE response or None if not found.""" + try: + state = here_data[sensor_number][attribute_name] + except KeyError: + return None + state = convert_asterisk_to_none(state) + return state + + +def convert_asterisk_to_none(state: str) -> str | None: + """Convert HERE API representation of None.""" + if state == "*": + return None + return state diff --git a/custom_components/here_weather/weather.py b/custom_components/here_weather/weather.py new file mode 100644 index 00000000..524d7c3e --- /dev/null +++ b/custom_components/here_weather/weather.py @@ -0,0 +1,296 @@ +"""Weather platform for the HERE Destination Weather service.""" +from __future__ import annotations + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_PRESSURE, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + Forecast, + WeatherEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONDITION_CLASSES, + DEFAULT_MODE, + DOMAIN, + MODE_ASTRONOMY, + MODE_DAILY_SIMPLE, + SENSOR_TYPES, +) +from .utils import ( + convert_temperature_unit_of_measurement_if_needed, + get_attribute_from_here_data, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Add here_weather entities from a ConfigEntry.""" + here_weather_coordinators = hass.data[DOMAIN][entry.entry_id] + + entities_to_add = [] + for sensor_type in SENSOR_TYPES: + if sensor_type != MODE_ASTRONOMY: + entities_to_add.append( + HEREDestinationWeather( + entry, + here_weather_coordinators[sensor_type], + sensor_type, + ) + ) + async_add_entities(entities_to_add) + + +class HEREDestinationWeather(CoordinatorEntity, WeatherEntity): + """Implementation of an HERE Destination Weather WeatherEntity.""" + + def __init__( + self, entry: ConfigEntry, coordinator: DataUpdateCoordinator, mode: str + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = entry.data[CONF_NAME] + self._mode = mode + self._unique_id = "".join( + f"{entry.data[CONF_LATITUDE]}_{entry.data[CONF_LONGITUDE]}_{self._mode}".lower().split() + ) + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {self._mode}" + + @property + def unique_id(self): + """Set unique_id for sensor.""" + return self._unique_id + + @property + def condition(self) -> str | None: + """Return the current condition.""" + return get_condition_from_here_data(self.coordinator.data) + + @property + def temperature(self) -> float | None: + """Return the temperature.""" + return get_temperature_from_here_data(self.coordinator.data, self._mode) + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return convert_temperature_unit_of_measurement_if_needed( + self.coordinator.hass.config.units.name, TEMP_CELSIUS + ) + + @property + def pressure(self) -> float | None: + """Return the pressure.""" + return get_pressure_from_here_data(self.coordinator.data, self._mode) + + @property + def humidity(self) -> float | None: + """Return the humidity.""" + if ( + humidity := get_attribute_from_here_data(self.coordinator.data, "humidity") + ) is not None: + return float(humidity) + return None + + @property + def wind_speed(self) -> float | None: + """Return the wind speed.""" + return get_wind_speed_from_here_data(self.coordinator.data) + + @property + def wind_bearing(self) -> float | str | None: + """Return the wind bearing.""" + return get_wind_bearing_from_here_data(self.coordinator.data) + + @property + def attribution(self) -> str | None: + """Return the attribution.""" + return None + + @property + def visibility(self) -> float | None: + """Return the visibility.""" + if "visibility" in SENSOR_TYPES[self._mode]: + if ( + visibility := get_attribute_from_here_data( + self.coordinator.data, "visibility" + ) + ) is not None: + return float(visibility) + return None + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast array.""" + data: list[Forecast] = [] + for offset in range(len(self.coordinator.data)): + data.append( + { + ATTR_FORECAST_CONDITION: get_condition_from_here_data( + self.coordinator.data, offset + ), + ATTR_FORECAST_TIME: get_time_from_here_data( + self.coordinator.data, offset + ), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: get_precipitation_probability( + self.coordinator.data, self._mode, offset + ), + ATTR_FORECAST_PRECIPITATION: calc_precipitation( + self.coordinator.data, offset + ), + ATTR_FORECAST_PRESSURE: get_pressure_from_here_data( + self.coordinator.data, self._mode, offset + ), + ATTR_FORECAST_TEMP: get_high_or_default_temperature_from_here_data( + self.coordinator.data, self._mode, offset + ), + ATTR_FORECAST_TEMP_LOW: get_low_or_default_temperature_from_here_data( + self.coordinator.data, self._mode, offset + ), + ATTR_FORECAST_WIND_BEARING: get_wind_bearing_from_here_data( + self.coordinator.data, offset + ), + ATTR_FORECAST_WIND_SPEED: get_wind_speed_from_here_data( + self.coordinator.data, offset + ), + } + ) + return data + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return self._mode == DEFAULT_MODE + + @property + def device_info(self) -> DeviceInfo: + """Return a device description for device registry.""" + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": self.name, + "manufacturer": "here.com", + "entry_type": "service", + } + + +def get_wind_speed_from_here_data(here_data: list, offset: int = 0) -> float: + """Return the wind speed from here_data.""" + wind_speed = get_attribute_from_here_data(here_data, "windSpeed", offset) + assert wind_speed is not None + return float(wind_speed) + + +def get_wind_bearing_from_here_data(here_data: list, offset: int = 0) -> int: + """Return the wind bearing from here_data.""" + wind_bearing = get_attribute_from_here_data(here_data, "windDirection", offset) + assert wind_bearing is not None + return int(wind_bearing) + + +def get_time_from_here_data(here_data: list, offset: int = 0) -> str: + """Return the time from here_data.""" + time = get_attribute_from_here_data(here_data, "utcTime", offset) + assert time is not None + return time + + +def get_pressure_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> float | None: + """Return the pressure from here_data.""" + if "barometerPressure" in SENSOR_TYPES[mode]: + if ( + pressure := get_attribute_from_here_data( + here_data, "barometerPressure", offset + ) + ) is not None: + return float(pressure) + return None + + +def get_precipitation_probability( + here_data: list, mode: str, offset: int = 0 +) -> int | None: + """Return the precipitation probability from here_data.""" + if "precipitationProbability" in SENSOR_TYPES[mode]: + if ( + precipitation_probability := get_attribute_from_here_data( + here_data, "precipitationProbability", offset + ) + ) is not None: + return int(precipitation_probability) + return None + + +def get_condition_from_here_data(here_data: list, offset: int = 0) -> str | None: + """Return the condition from here_data.""" + return next( + ( + k + for k, v in CONDITION_CLASSES.items() + if get_attribute_from_here_data(here_data, "iconName", offset) in v + ), + None, + ) + + +def get_high_or_default_temperature_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> float | None: + """Return the temperature from here_data.""" + temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) + if temperature is not None: + return float(temperature) + + return get_temperature_from_here_data(here_data, mode, offset) + + +def get_low_or_default_temperature_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> float | None: + """Return the temperature from here_data.""" + temperature = get_attribute_from_here_data(here_data, "lowTemperature", offset) + if temperature is not None: + return float(temperature) + return get_temperature_from_here_data(here_data, mode, offset) + + +def get_temperature_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> float | None: + """Return the temperature from here_data.""" + if mode == MODE_DAILY_SIMPLE: + temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) + else: + temperature = get_attribute_from_here_data(here_data, "temperature", offset) + if temperature is not None: + return float(temperature) + return None + + +def calc_precipitation(here_data: list, offset: int = 0) -> float | None: + """Calculate Precipitation.""" + rain_fall = get_attribute_from_here_data(here_data, "rainFall", offset) + snow_fall = get_attribute_from_here_data(here_data, "snowFall", offset) + if rain_fall is not None and snow_fall is not None: + return float(rain_fall) + float(snow_fall) + return None diff --git a/custom_components/ssh/manifest.json b/custom_components/ssh/manifest.json index 14cb2589..3750ed71 100644 --- a/custom_components/ssh/manifest.json +++ b/custom_components/ssh/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://github.com/custom-components/sensor.ssh", "requirements": ["pexpect==4.6.0"], "dependencies": [], - "codeowners": ["@jchasey"] + "codeowners": ["@jchasey"], + "version":"0.1.5" } diff --git a/www/community/lovelace-auto-entities/auto-entities.js b/www/community/lovelace-auto-entities/auto-entities.js index fe39af4d..e34de1c5 100644 --- a/www/community/lovelace-auto-entities/auto-entities.js +++ b/www/community/lovelace-auto-entities/auto-entities.js @@ -1,4 +1,4 @@ -function t(t,e,i,s){var n,o=arguments.length,r=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,s);else for(var a=t.length-1;a>=0;a--)(n=t[a])&&(r=(o<3?n(r):o>3?n(e,i,r):n(e,i))||r);return o>3&&r&&Object.defineProperty(e,i,r),r}const e="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,i=(t,e,i=null)=>{for(;e!==i;){const i=e.nextSibling;t.removeChild(e),e=i}},s=`{{lit-${String(Math.random()).slice(2)}}}`,n=`\x3c!--${s}--\x3e`,o=new RegExp(`${s}|${n}`);class r{constructor(t,e){this.parts=[],this.element=e;const i=[],n=[],r=document.createTreeWalker(e.content,133,null,!1);let c=0,h=-1,u=0;const{strings:p,values:{length:f}}=t;for(;u0;){const e=p[u],i=d.exec(e)[2],s=i.toLowerCase()+"$lit$",n=t.getAttribute(s);t.removeAttribute(s);const r=n.split(o);this.parts.push({type:"attribute",index:h,name:i,strings:r}),u+=r.length-1}}"TEMPLATE"===t.tagName&&(n.push(t),r.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(s)>=0){const s=t.parentNode,n=e.split(o),r=n.length-1;for(let e=0;e{const i=t.length-e.length;return i>=0&&t.slice(i)===e},c=t=>-1!==t.index,l=()=>document.createComment(""),d=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function h(t,e){const{element:{content:i},parts:s}=t,n=document.createTreeWalker(i,133,null,!1);let o=p(s),r=s[o],a=-1,c=0;const l=[];let d=null;for(;n.nextNode();){a++;const t=n.currentNode;for(t.previousSibling===d&&(d=null),e.has(t)&&(l.push(t),null===d&&(d=t)),null!==d&&c++;void 0!==r&&r.index===a;)r.index=null!==d?-1:r.index-c,o=p(s,o),r=s[o]}l.forEach((t=>t.parentNode.removeChild(t)))}const u=t=>{let e=11===t.nodeType?0:1;const i=document.createTreeWalker(t,133,null,!1);for(;i.nextNode();)e++;return e},p=(t,e=-1)=>{for(let i=e+1;i"function"==typeof t&&f.has(t),_={},v={};class m{constructor(t,e,i){this.__parts=[],this.template=t,this.processor=e,this.options=i}update(t){let e=0;for(const i of this.__parts)void 0!==i&&i.setValue(t[e]),e++;for(const t of this.__parts)void 0!==t&&t.commit()}_clone(){const t=e?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),i=[],s=this.template.parts,n=document.createTreeWalker(t,133,null,!1);let o,r=0,a=0,l=n.nextNode();for(;rt}),b=` ${s} `;class w{constructor(t,e,i,s){this.strings=t,this.values=e,this.type=i,this.processor=s}getHTML(){const t=this.strings.length-1;let e="",i=!1;for(let o=0;o-1||i)&&-1===t.indexOf("--\x3e",r+1);const a=d.exec(t);e+=null===a?t+(i?b:n):t.substr(0,a.index)+a[1]+a[2]+"$lit$"+a[3]+s}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");let e=this.getHTML();return void 0!==y&&(e=y.createHTML(e)),t.innerHTML=e,t}}const S=t=>null===t||!("object"==typeof t||"function"==typeof t),C=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class O{constructor(t,e,i){this.dirty=!0,this.element=t,this.name=e,this.strings=i,this.parts=[];for(let t=0;t{try{const t={get capture(){return N=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class T{constructor(t,e,i){this.value=void 0,this.__pendingValue=void 0,this.element=t,this.eventName=e,this.eventContext=i,this.__boundHandleEvent=t=>this.handleEvent(t)}setValue(t){this.__pendingValue=t}commit(){for(;g(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=_,t(this)}if(this.__pendingValue===_)return;const t=this.__pendingValue,e=this.value,i=null==t||null!=e&&(t.capture!==e.capture||t.once!==e.once||t.passive!==e.passive),s=null!=t&&(null==e||i);i&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),s&&(this.__options=k(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=_}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const k=t=>t&&(N?{capture:t.capture,passive:t.passive,once:t.once}:t.capture);function A(t){let e=M.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},M.set(t.type,e));let i=e.stringsArray.get(t.strings);if(void 0!==i)return i;const n=t.strings.join(s);return i=e.keyString.get(n),void 0===i&&(i=new r(t,t.getTemplateElement()),e.keyString.set(n,i)),e.stringsArray.set(t.strings,i),i}const M=new Map,U=new WeakMap;const F=new class{handleAttributeExpressions(t,e,i,s){const n=e[0];if("."===n){return new P(t,e.slice(1),i).parts}if("@"===n)return[new T(t,e.slice(1),s.eventContext)];if("?"===n)return[new j(t,e.slice(1),i)];return new O(t,e,i).parts}handleTextExpression(t){return new x(t)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.3.0");const V=(t,...e)=>new w(t,e,"html",F),I=(t,e)=>`${t}--${e}`;let R=!0;void 0===window.ShadyCSS?R=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),R=!1);const D=t=>e=>{const i=I(e.type,t);let n=M.get(i);void 0===n&&(n={stringsArray:new WeakMap,keyString:new Map},M.set(i,n));let o=n.stringsArray.get(e.strings);if(void 0!==o)return o;const a=e.strings.join(s);if(o=n.keyString.get(a),void 0===o){const i=e.getTemplateElement();R&&window.ShadyCSS.prepareTemplateDom(i,t),o=new r(e,i),n.keyString.set(a,o)}return n.stringsArray.set(e.strings,o),o},q=["html","svg"],G=new Set,W=(t,e,i)=>{G.add(t);const s=i?i.element:document.createElement("template"),n=e.querySelectorAll("style"),{length:o}=n;if(0===o)return void window.ShadyCSS.prepareTemplateStyles(s,t);const r=document.createElement("style");for(let t=0;t{q.forEach((e=>{const i=M.get(I(e,t));void 0!==i&&i.keyString.forEach((t=>{const{element:{content:e}}=t,i=new Set;Array.from(e.querySelectorAll("style")).forEach((t=>{i.add(t)})),h(t,i)}))}))})(t);const a=s.content;i?function(t,e,i=null){const{element:{content:s},parts:n}=t;if(null==i)return void s.appendChild(e);const o=document.createTreeWalker(s,133,null,!1);let r=p(n),a=0,c=-1;for(;o.nextNode();)for(c++,o.currentNode===i&&(a=u(e),i.parentNode.insertBefore(e,i));-1!==r&&n[r].index===c;){if(a>0){for(;-1!==r;)n[r].index+=a,r=p(n,r);return}r=p(n,r)}}(i,r,a.firstChild):a.insertBefore(r,a.firstChild),window.ShadyCSS.prepareTemplateStyles(s,t);const c=a.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==c)e.insertBefore(c.cloneNode(!0),e.firstChild);else if(i){a.insertBefore(r,a.firstChild);const t=new Set;t.add(r),h(i,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const z={toAttribute(t,e){switch(e){case Boolean:return t?"":null;case Object:case Array:return null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){switch(e){case Boolean:return null!==t;case Number:return null===t?null:Number(t);case Object:case Array:return JSON.parse(t)}return t}},L=(t,e)=>e!==t&&(e==e||t==t),B={attribute:!0,type:String,converter:z,reflect:!1,hasChanged:L};class H extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach(((e,i)=>{const s=this._attributeNameForProperty(i,e);void 0!==s&&(this._attributeToPropertyMap.set(s,i),t.push(s))})),t}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const t=Object.getPrototypeOf(this)._classProperties;void 0!==t&&t.forEach(((t,e)=>this._classProperties.set(e,t)))}}static createProperty(t,e=B){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const i="symbol"==typeof t?Symbol():`__${t}`,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const n=this[t];this[e]=s,this.requestUpdateInternal(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||B}static finalize(){const t=Object.getPrototypeOf(this);if(t.hasOwnProperty("finalized")||t.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const t=this.properties,e=[...Object.getOwnPropertyNames(t),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t):[]];for(const i of e)this.createProperty(i,t[i])}}static _attributeNameForProperty(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}static _valueHasChanged(t,e,i=L){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,s=e.converter||z,n="function"==typeof s?s:s.fromAttribute;return n?n(t,i):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const i=e.type,s=e.converter;return(s&&s.toAttribute||z.toAttribute)(t,i)}initialize(){this._updateState=0,this._updatePromise=new Promise((t=>this._enableUpdatingResolver=t)),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach(((t,e)=>{if(this.hasOwnProperty(e)){const t=this[e];delete this[e],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(e,t)}}))}_applyInstanceProperties(){this._instanceProperties.forEach(((t,e)=>this[e]=t)),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(t,e,i){e!==i&&this._attributeToProperty(t,i)}_propertyToAttribute(t,e,i=B){const s=this.constructor,n=s._attributeNameForProperty(t,i);if(void 0!==n){const t=s._propertyValueToAttribute(e,i);if(void 0===t)return;this._updateState=8|this._updateState,null==t?this.removeAttribute(n):this.setAttribute(n,t),this._updateState=-9&this._updateState}}_attributeToProperty(t,e){if(8&this._updateState)return;const i=this.constructor,s=i._attributeToPropertyMap.get(t);if(void 0!==s){const t=i.getPropertyOptions(s);this._updateState=16|this._updateState,this[s]=i._propertyValueFromAttribute(e,t),this._updateState=-17&this._updateState}}requestUpdateInternal(t,e,i){let s=!0;if(void 0!==t){const n=this.constructor;i=i||n.getPropertyOptions(t),n._valueHasChanged(this[t],e,i.hasChanged)?(this._changedProperties.has(t)||this._changedProperties.set(t,e),!0!==i.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(t,i))):s=!1}!this._hasRequestedUpdate&&s&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(t,e){return this.requestUpdateInternal(t,e),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(t){}const t=this.performUpdate();return null!=t&&await t,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){if(!this._hasRequestedUpdate)return;this._instanceProperties&&this._applyInstanceProperties();let t=!1;const e=this._changedProperties;try{t=this.shouldUpdate(e),t?this.update(e):this._markUpdated()}catch(e){throw t=!1,this._markUpdated(),e}t&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(e)),this.updated(e))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(t){return!0}update(t){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach(((t,e)=>this._propertyToAttribute(e,this[e],t))),this._reflectingProperties=void 0),this._markUpdated()}updated(t){}firstUpdated(t){}}H.finalized=!0;const J=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?Object.assign(Object.assign({},e),{finisher(i){i.createProperty(e.key,t)}}):{kind:"field",key:Symbol(),placement:"own",descriptor:{},initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function K(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):J(t,e)}function Y(t){return K({attribute:!1,hasChanged:null==t?void 0:t.hasChanged})}const Q=(t,e,i)=>{Object.defineProperty(e,i,t)},X=(t,e)=>({kind:"method",placement:"prototype",key:e.key,descriptor:t}),Z=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,tt=Symbol();class et{constructor(t,e){if(e!==tt)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(Z?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const it=(t,...e)=>{const i=e.reduce(((e,i,s)=>e+(t=>{if(t instanceof et)return t.cssText;if("number"==typeof t)return t;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[s+1]),t[0]);return new et(i,tt)};(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");const st={};class nt extends H{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const t=this.getStyles();if(Array.isArray(t)){const e=(t,i)=>t.reduceRight(((t,i)=>Array.isArray(i)?e(i,t):(t.add(i),t)),i),i=e(t,new Set),s=[];i.forEach((t=>s.unshift(t))),this._styles=s}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!Z){const e=Array.prototype.slice.call(t.cssRules).reduce(((t,e)=>t+e.cssText),"");return new et(String(e),tt)}return t}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?Z?this.renderRoot.adoptedStyleSheets=t.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map((t=>t.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){const e=this.render();super.update(t),e!==st&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)})))}render(){return st}}function ot(){return document.querySelector("hc-main")?document.querySelector("hc-main").hass:document.querySelector("home-assistant")?document.querySelector("home-assistant").hass:void 0}nt.finalized=!0,nt.render=(t,e,s)=>{if(!s||"object"!=typeof s||!s.scopeName)throw new Error("The `scopeName` option is required.");const n=s.scopeName,o=U.has(e),r=R&&11===e.nodeType&&!!e.host,a=r&&!G.has(n),c=a?document.createDocumentFragment():e;if(((t,e,s)=>{let n=U.get(e);void 0===n&&(i(e,e.firstChild),U.set(e,n=new x(Object.assign({templateFactory:A},s))),n.appendInto(e)),n.setValue(t),n.commit()})(t,c,Object.assign({templateFactory:D(n)},s)),a){const t=U.get(c);U.delete(c);const s=t.value instanceof m?t.value.template:void 0;W(n,c,s),i(e,e.firstChild),e.appendChild(c),U.set(e,t)}!o&&r&&window.ShadyCSS.styleElement(e.host)};const rt="lovelace-player-device-id";function at(){if(!localStorage[rt]){const t=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);window.fully&&"function"==typeof fully.getDeviceId?localStorage[rt]=fully.getDeviceId():localStorage[rt]=`${t()}${t()}-${t()}${t()}`}return localStorage[rt]}let ct=at();const lt=new URLSearchParams(window.location.search);var dt;function ht(t){return!!String(t).includes("{%")||(!!String(t).includes("{{")||void 0)}lt.get("deviceID")&&null!==(dt=lt.get("deviceID"))&&("clear"===dt?localStorage.removeItem(rt):localStorage[rt]=dt,ct=at()),window.cardMod_template_cache=window.cardMod_template_cache||{};const ut=window.cardMod_template_cache;async function pt(t,e,i){const s=ot().connection,n=JSON.stringify([e,i]);let o=ut[n];o?(o.callbacks.has(t)||ft(t),t(o.value),o.callbacks.add(t)):(ft(t),t(""),i=Object.assign({user:ot().user.name,browser:ct,hash:location.hash.substr(1)||""},i),ut[n]=o={template:e,variables:i,value:"",callbacks:new Set([t]),unsubscribe:s.subscribeMessage((t=>function(t,e){const i=ut[t];i&&(i.value=e.result,i.callbacks.forEach((t=>t(e.result))))}(n,t)),{type:"render_template",template:e,variables:i})})}async function ft(t){let e;for(const[i,s]of Object.entries(ut))if(s.callbacks.has(t)){s.callbacks.delete(t),0==s.callbacks.size&&(e=s.unsubscribe,delete ut[i]);break}e&&await(await e)()}var gt;function _t(t,e){if("string"==typeof e&&"string"==typeof t&&(t.startsWith("/")&&t.endsWith("/")||-1!==t.indexOf("*"))){return t.startsWith("/")||(t=`/^${t=t.replace(/\./g,".").replace(/\*/g,".*")}$/`),new RegExp(t.slice(1,-1)).test(e)}if("string"==typeof t){if(t.startsWith("<="))return parseFloat(e)<=parseFloat(t.substr(2));if(t.startsWith(">="))return parseFloat(e)>=parseFloat(t.substr(2));if(t.startsWith("<"))return parseFloat(e)"))return parseFloat(e)>parseFloat(t.substr(1));if(t.startsWith("!"))return parseFloat(e)!=parseFloat(t.substr(1));if(t.startsWith("="))return parseFloat(e)==parseFloat(t.substr(1))}return t===e}window.autoEntities_cache=null!==(gt=window.autoEntities_cache)&&void 0!==gt?gt:{};const vt=window.autoEntities_cache;async function mt(t){var e;return vt.areas=null!==(e=vt.areas)&&void 0!==e?e:await t.callWS({type:"config/area_registry/list"}),vt.areas}async function yt(t){var e;return vt.devices=null!==(e=vt.devices)&&void 0!==e?e:await t.callWS({type:"config/device_registry/list"}),vt.devices}async function bt(t){var e;return vt.entities=null!==(e=vt.entities)&&void 0!==e?e:await t.callWS({type:"config/entity_registry/list"}),vt.entities}const wt={options:async()=>!0,sort:async()=>!0,domain:async(t,e,i)=>_t(e,i.entity_id.split(".")[0]),entity_id:async(t,e,i)=>_t(e,i.entity_id),state:async(t,e,i)=>_t(e,i.state),name:async(t,e,i)=>{var s;return _t(e,null===(s=i.attributes)||void 0===s?void 0:s.friendly_name)},group:async(t,e,i)=>{var s,n,o;return null===(o=null===(n=null===(s=t.states[e])||void 0===s?void 0:s.attributes)||void 0===n?void 0:n.entity_id)||void 0===o?void 0:o.includes(i.entity_id)},attributes:async(t,e,i)=>{for(const[t,s]of Object.entries(e)){let e=t.split(" ")[0],n=i.attributes;for(const t of e.split(":"))n=n?n[t]:void 0;if(void 0===n||!_t(s,n))return!1}return!0},not:async(t,e,i)=>!await St(t,e,i.entity_id),or:async(t,e,i)=>{for(const s of e)if(await St(t,s,i.entity_id))return!0;return!1},device:async(t,e,i)=>{const s=(await bt(t)).find((t=>t.entity_id===i.entity_id));if(!s)return!1;const n=(await yt(t)).find((t=>t.id===s.device_id));return!!n&&(_t(e,n.name_by_user)||_t(e,n.name))},area:async(t,e,i)=>{const s=(await bt(t)).find((t=>t.entity_id===i.entity_id));if(!s)return!1;let n=(await mt(t)).find((t=>t.area_id===s.area_id));if(n)return _t(e,n.name);const o=(await yt(t)).find((t=>t.id===s.device_id));return!!o&&(n=(await mt(t)).find((t=>t.area_id===o.area_id)),!!n&&_t(e,n.name))},last_changed:async(t,e,i)=>_t(e,((new Date).getTime()-new Date(i.last_changed).getTime())/6e4),last_updated:async(t,e,i)=>_t(e,((new Date).getTime()-new Date(i.last_updated).getTime())/6e4),last_triggered:async(t,e,i)=>{if(null==i.attributes.last_triggered)return!1;return _t(e,((new Date).getTime()-new Date(i.attributes.last_triggered).getTime())/6e4)}};async function St(t,e,i){var s;if(!t.states[i])return!1;for(let[n,o]of Object.entries(e))if(n=n.trim().split(" ")[0].trim(),!await(null===(s=wt[n])||void 0===s?void 0:s.call(wt,t,o,t.states[i])))return!1;return!0}function Ct(t,e,i){var s,n,o,r;const[a,c]=i.reverse?[-1,1]:[1,-1];return i.ignore_case&&(t=null!==(n=null===(s=null==t?void 0:t.toLowerCase)||void 0===s?void 0:s.call(t))&&void 0!==n?n:t,e=null!==(r=null===(o=null==e?void 0:e.toLowerCase)||void 0===o?void 0:o.call(e))&&void 0!==r?r:e),i.numeric&&(isNaN(parseFloat(t))&&isNaN(parseFloat(e))||(t=isNaN(parseFloat(t))?void 0:parseFloat(t),e=isNaN(parseFloat(e))?void 0:parseFloat(e))),void 0===t&&void 0===e?0:void 0===t?a:void 0===e||te?a:0}const Ot={none:()=>0,domain:(t,e,i)=>{var s,n;return Ct(null===(s=null==t?void 0:t.entity_id)||void 0===s?void 0:s.split(".")[0],null===(n=null==e?void 0:e.entity_id)||void 0===n?void 0:n.split(".")[0],i)},entity_id:(t,e,i)=>Ct(null==t?void 0:t.entity_id,null==e?void 0:e.entity_id,i),friendly_name:(t,e,i)=>{var s,n,o,r;return Ct((null===(s=null==t?void 0:t.attributes)||void 0===s?void 0:s.friendly_name)||(null===(n=null==t?void 0:t.entity_id)||void 0===n?void 0:n.split(".")[1]),(null===(o=null==e?void 0:e.attributes)||void 0===o?void 0:o.friendly_name)||(null===(r=null==e?void 0:e.entity_id)||void 0===r?void 0:r.split(".")[1]),i)},name:(t,e,i)=>{var s,n,o,r;return Ct((null===(s=null==t?void 0:t.attributes)||void 0===s?void 0:s.friendly_name)||(null===(n=null==t?void 0:t.entity_id)||void 0===n?void 0:n.split(".")[1]),(null===(o=null==e?void 0:e.attributes)||void 0===o?void 0:o.friendly_name)||(null===(r=null==e?void 0:e.entity_id)||void 0===r?void 0:r.split(".")[1]),i)},state:(t,e,i)=>Ct(null==t?void 0:t.state,null==e?void 0:e.state,i),attribute:(t,e,i)=>{var s;const[n,o]=(null==i?void 0:i.reverse)?[-1,1]:[1,-1];let r=null==t?void 0:t.attributes,a=null==e?void 0:e.attributes;for(const t of null===(s=null==i?void 0:i.attribute)||void 0===s?void 0:s.split(":")){if(void 0===r&&void 0===a)return 0;if(void 0===r)return n;if(void 0===a)return o;[r,a]=[r[t],a[t]]}return Ct(r,a,i)},last_changed:(t,e,i)=>(i.numeric=!0,Ct(new Date(null==t?void 0:t.last_changed).getTime(),new Date(null==e?void 0:e.last_changed).getTime(),i)),last_updated:(t,e,i)=>(i.numeric=!0,Ct(new Date(null==t?void 0:t.last_updated).getTime(),new Date(null==e?void 0:e.last_updated).getTime(),i)),last_triggered:(t,e,i)=>{var s,n,o,r;return null==(null===(s=null==t?void 0:t.attributes)||void 0===s?void 0:s.last_triggered)||null==(null===(n=null==e?void 0:e.attributes)||void 0===n?void 0:n.last_triggered)?0:(i.numeric=!0,Ct(new Date(null===(o=null==t?void 0:t.attributes)||void 0===o?void 0:o.last_triggered).getTime(),new Date(null===(r=null==e?void 0:e.attributes)||void 0===r?void 0:r.last_triggered).getTime(),i))}};function Et(t,e){return function(i,s){var n,o;return null!==(o=null===(n=Ot[e.method])||void 0===n?void 0:n.call(Ot,t.states[i.entity],t.states[s.entity],e))&&void 0!==o?o:0}}var xt="1.8.5";const jt=["domain","entity_id","state","name","group","device","area","last_changed","last_updated","last_triggered"],Pt=["none","domain","entity_id","friendly_name","state","last_changed","last_updated","last_triggered"];class $t extends nt{constructor(){super(...arguments),this._selectedTab=0,this._cardGUIMode=!0,this._cardGUIModeAvailable=!0}setConfig(t){this._config=t}_handleSwitchTab(t){this._selectedTab=parseInt(t.detail.index,10)}_addFilterGroup(){var t;if(!this._config)return;const e=[...null===(t=this._config.filter)||void 0===t?void 0:t.include];e.push({domain:""});const i=Object.assign(Object.assign({},this._config.filter),{include:e});this._config=Object.assign(Object.assign({},this._config),{filter:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_deleteFilterGroup(t){var e;if(!this._config)return;const i=[...null===(e=this._config.filter)||void 0===e?void 0:e.include];i.splice(t,1);const s=Object.assign(Object.assign({},this._config.filter),{include:i});this._config=Object.assign(Object.assign({},this._config),{filter:s}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_moveFilterGroup(t,e){var i;if(!this._config)return;const s=[...null===(i=this._config.filter)||void 0===i?void 0:i.include];[s[t],s[t+e]]=[s[t+e],s[t]];const n=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:n}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_addSpecialEntry(){var t;if(!this._config)return;const e=[...null===(t=this._config.filter)||void 0===t?void 0:t.include];e.push({type:""});const i=Object.assign(Object.assign({},this._config.filter),{include:e});this._config=Object.assign(Object.assign({},this._config),{filter:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}async _changeSpecialEntry(t,e){var i;if(!this._config)return;const s=[...null===(i=this._config.filter)||void 0===i?void 0:i.include];s[t]=e.detail.value;const n=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:n}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}async _changeGroupOptions(t,e){var i;if(!this._config)return;const s=e.detail.value,n=[...null===(i=this._config.filter)||void 0===i?void 0:i.include];n[t]=Object.assign(Object.assign({},n[t]),{options:s});const o=Object.assign(Object.assign({},this._config.filter),{include:n});this._config=Object.assign(Object.assign({},this._config),{filter:o}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_addFilter(t){var e;if(!this._config)return;const i=jt.find((e=>void 0===this._config.filter.include[t][e]));if(void 0===i)return;const s=[...null===(e=this._config.filter)||void 0===e?void 0:e.include];s[t]=Object.assign(Object.assign({},s[t]),{[i]:""});const n=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:n}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_removeFilter(t,e){var i;if(!this._config)return;const s=[...null===(i=this._config.filter)||void 0===i?void 0:i.include],n=Object.assign({},s[t]);if(delete n[e],0===Object.keys(n).length)return this._deleteFilterGroup(t);s[t]=n;const o=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:o}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeFilterKey(t,e,i){var s;if(!this._config)return;const n=jt[i.target.selected];if(void 0===n||n===e)return;const o=[...null===(s=this._config.filter)||void 0===s?void 0:s.include],r=Object.assign({},o[t]);if(void 0===r[e])return;r[n]=r[e],delete r[e],o[t]=r;const a=Object.assign(Object.assign({},this._config.filter),{include:o});this._config=Object.assign(Object.assign({},this._config),{filter:a}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeFilterValue(t,e,i){var s;if(!this._config)return;const n=[...null===(s=this._config.filter)||void 0===s?void 0:s.include],o=Object.assign({},n[t]);o[e]=i.target.value,n[t]=o;const r=Object.assign(Object.assign({},this._config.filter),{include:n});this._config=Object.assign(Object.assign({},this._config),{filter:r}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeSortMethod(t){if(!this._config)return;const e=Pt[t.target.selected],i=Object.assign(Object.assign({},this._config.sort),{method:e});this._config=Object.assign(Object.assign({},this._config),{sort:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_sortOptionToggle(t,e){if(!this._config)return;const i=Object.assign({},this._config.sort);i[t]=e.target.checked,this._config=Object.assign(Object.assign({},this._config),{sort:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_showEmptyToggle(){if(!this._config)return;const t=!1===this._config.show_empty;this._config=Object.assign(Object.assign({},this._config),{show_empty:t}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeCardParam(t){if(!this._config)return;const e=""===t.target.value||"entities"===t.target.value?void 0:t.target.value;this._config=Object.assign(Object.assign({},this._config),{card_param:e}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_getCardConfig(){const t=Object.assign({},this._config.card);return t[this._config.card_param||"entities"]=[],t}_handleCardPicked(t){if(t.stopPropagation(),!this._config)return;const e=Object.assign({},t.detail.config);delete e.entities,this._config=Object.assign(Object.assign({},this._config),{card:e}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_handleCardConfigChanged(t){if(t.stopPropagation(),!this._config)return;const e=Object.assign({},t.detail.config);delete e[this._config.card_param||"entities"],this._config=Object.assign(Object.assign({},this._config),{card:e}),this._cardGUIModeAvailable=t.detail.guiModeAvailable,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_deleteCard(t){this._config&&(this._config=Object.assign({},this._config),delete this._config.card,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_toggleCardMode(t){var e;null===(e=this._cardEditorEl)||void 0===e||e.toggleMode()}_cardGUIModeChanged(t){t.stopPropagation(),this._cardGUIMode=t.detail.guiMode,this._cardGUIModeAvailable=t.detail.guiModeAvailable}render(){return this.hass&&this._config?V` +function t(t,e,i,s){var n,o=arguments.length,r=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,s);else for(var a=t.length-1;a>=0;a--)(n=t[a])&&(r=(o<3?n(r):o>3?n(e,i,r):n(e,i))||r);return o>3&&r&&Object.defineProperty(e,i,r),r}const e="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,i=(t,e,i=null)=>{for(;e!==i;){const i=e.nextSibling;t.removeChild(e),e=i}},s=`{{lit-${String(Math.random()).slice(2)}}}`,n=`\x3c!--${s}--\x3e`,o=new RegExp(`${s}|${n}`);class r{constructor(t,e){this.parts=[],this.element=e;const i=[],n=[],r=document.createTreeWalker(e.content,133,null,!1);let l=0,h=-1,u=0;const{strings:p,values:{length:f}}=t;for(;u0;){const e=p[u],i=d.exec(e)[2],s=i.toLowerCase()+"$lit$",n=t.getAttribute(s);t.removeAttribute(s);const r=n.split(o);this.parts.push({type:"attribute",index:h,name:i,strings:r}),u+=r.length-1}}"TEMPLATE"===t.tagName&&(n.push(t),r.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(s)>=0){const s=t.parentNode,n=e.split(o),r=n.length-1;for(let e=0;e{const i=t.length-e.length;return i>=0&&t.slice(i)===e},l=t=>-1!==t.index,c=()=>document.createComment(""),d=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function h(t,e){const{element:{content:i},parts:s}=t,n=document.createTreeWalker(i,133,null,!1);let o=p(s),r=s[o],a=-1,l=0;const c=[];let d=null;for(;n.nextNode();){a++;const t=n.currentNode;for(t.previousSibling===d&&(d=null),e.has(t)&&(c.push(t),null===d&&(d=t)),null!==d&&l++;void 0!==r&&r.index===a;)r.index=null!==d?-1:r.index-l,o=p(s,o),r=s[o]}c.forEach((t=>t.parentNode.removeChild(t)))}const u=t=>{let e=11===t.nodeType?0:1;const i=document.createTreeWalker(t,133,null,!1);for(;i.nextNode();)e++;return e},p=(t,e=-1)=>{for(let i=e+1;i"function"==typeof t&&f.has(t),_={},v={};class m{constructor(t,e,i){this.__parts=[],this.template=t,this.processor=e,this.options=i}update(t){let e=0;for(const i of this.__parts)void 0!==i&&i.setValue(t[e]),e++;for(const t of this.__parts)void 0!==t&&t.commit()}_clone(){const t=e?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),i=[],s=this.template.parts,n=document.createTreeWalker(t,133,null,!1);let o,r=0,a=0,c=n.nextNode();for(;rt}),b=` ${s} `;class w{constructor(t,e,i,s){this.strings=t,this.values=e,this.type=i,this.processor=s}getHTML(){const t=this.strings.length-1;let e="",i=!1;for(let o=0;o-1||i)&&-1===t.indexOf("--\x3e",r+1);const a=d.exec(t);e+=null===a?t+(i?b:n):t.substr(0,a.index)+a[1]+a[2]+"$lit$"+a[3]+s}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");let e=this.getHTML();return void 0!==y&&(e=y.createHTML(e)),t.innerHTML=e,t}}const S=t=>null===t||!("object"==typeof t||"function"==typeof t),C=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class O{constructor(t,e,i){this.dirty=!0,this.element=t,this.name=e,this.strings=i,this.parts=[];for(let t=0;t{try{const t={get capture(){return N=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class T{constructor(t,e,i){this.value=void 0,this.__pendingValue=void 0,this.element=t,this.eventName=e,this.eventContext=i,this.__boundHandleEvent=t=>this.handleEvent(t)}setValue(t){this.__pendingValue=t}commit(){for(;g(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=_,t(this)}if(this.__pendingValue===_)return;const t=this.__pendingValue,e=this.value,i=null==t||null!=e&&(t.capture!==e.capture||t.once!==e.once||t.passive!==e.passive),s=null!=t&&(null==e||i);i&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),s&&(this.__options=k(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=_}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const k=t=>t&&(N?{capture:t.capture,passive:t.passive,once:t.once}:t.capture);function A(t){let e=M.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},M.set(t.type,e));let i=e.stringsArray.get(t.strings);if(void 0!==i)return i;const n=t.strings.join(s);return i=e.keyString.get(n),void 0===i&&(i=new r(t,t.getTemplateElement()),e.keyString.set(n,i)),e.stringsArray.set(t.strings,i),i}const M=new Map,U=new WeakMap;const F=new class{handleAttributeExpressions(t,e,i,s){const n=e[0];if("."===n){return new j(t,e.slice(1),i).parts}if("@"===n)return[new T(t,e.slice(1),s.eventContext)];if("?"===n)return[new P(t,e.slice(1),i)];return new O(t,e,i).parts}handleTextExpression(t){return new x(t)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.3.0");const V=(t,...e)=>new w(t,e,"html",F),I=(t,e)=>`${t}--${e}`;let R=!0;void 0===window.ShadyCSS?R=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),R=!1);const D=t=>e=>{const i=I(e.type,t);let n=M.get(i);void 0===n&&(n={stringsArray:new WeakMap,keyString:new Map},M.set(i,n));let o=n.stringsArray.get(e.strings);if(void 0!==o)return o;const a=e.strings.join(s);if(o=n.keyString.get(a),void 0===o){const i=e.getTemplateElement();R&&window.ShadyCSS.prepareTemplateDom(i,t),o=new r(e,i),n.keyString.set(a,o)}return n.stringsArray.set(e.strings,o),o},q=["html","svg"],W=new Set,G=(t,e,i)=>{W.add(t);const s=i?i.element:document.createElement("template"),n=e.querySelectorAll("style"),{length:o}=n;if(0===o)return void window.ShadyCSS.prepareTemplateStyles(s,t);const r=document.createElement("style");for(let t=0;t{q.forEach((e=>{const i=M.get(I(e,t));void 0!==i&&i.keyString.forEach((t=>{const{element:{content:e}}=t,i=new Set;Array.from(e.querySelectorAll("style")).forEach((t=>{i.add(t)})),h(t,i)}))}))})(t);const a=s.content;i?function(t,e,i=null){const{element:{content:s},parts:n}=t;if(null==i)return void s.appendChild(e);const o=document.createTreeWalker(s,133,null,!1);let r=p(n),a=0,l=-1;for(;o.nextNode();)for(l++,o.currentNode===i&&(a=u(e),i.parentNode.insertBefore(e,i));-1!==r&&n[r].index===l;){if(a>0){for(;-1!==r;)n[r].index+=a,r=p(n,r);return}r=p(n,r)}}(i,r,a.firstChild):a.insertBefore(r,a.firstChild),window.ShadyCSS.prepareTemplateStyles(s,t);const l=a.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==l)e.insertBefore(l.cloneNode(!0),e.firstChild);else if(i){a.insertBefore(r,a.firstChild);const t=new Set;t.add(r),h(i,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const z={toAttribute(t,e){switch(e){case Boolean:return t?"":null;case Object:case Array:return null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){switch(e){case Boolean:return null!==t;case Number:return null===t?null:Number(t);case Object:case Array:return JSON.parse(t)}return t}},L=(t,e)=>e!==t&&(e==e||t==t),B={attribute:!0,type:String,converter:z,reflect:!1,hasChanged:L};class H extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach(((e,i)=>{const s=this._attributeNameForProperty(i,e);void 0!==s&&(this._attributeToPropertyMap.set(s,i),t.push(s))})),t}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const t=Object.getPrototypeOf(this)._classProperties;void 0!==t&&t.forEach(((t,e)=>this._classProperties.set(e,t)))}}static createProperty(t,e=B){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const i="symbol"==typeof t?Symbol():`__${t}`,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const n=this[t];this[e]=s,this.requestUpdateInternal(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||B}static finalize(){const t=Object.getPrototypeOf(this);if(t.hasOwnProperty("finalized")||t.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const t=this.properties,e=[...Object.getOwnPropertyNames(t),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t):[]];for(const i of e)this.createProperty(i,t[i])}}static _attributeNameForProperty(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}static _valueHasChanged(t,e,i=L){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,s=e.converter||z,n="function"==typeof s?s:s.fromAttribute;return n?n(t,i):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const i=e.type,s=e.converter;return(s&&s.toAttribute||z.toAttribute)(t,i)}initialize(){this._updateState=0,this._updatePromise=new Promise((t=>this._enableUpdatingResolver=t)),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach(((t,e)=>{if(this.hasOwnProperty(e)){const t=this[e];delete this[e],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(e,t)}}))}_applyInstanceProperties(){this._instanceProperties.forEach(((t,e)=>this[e]=t)),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(t,e,i){e!==i&&this._attributeToProperty(t,i)}_propertyToAttribute(t,e,i=B){const s=this.constructor,n=s._attributeNameForProperty(t,i);if(void 0!==n){const t=s._propertyValueToAttribute(e,i);if(void 0===t)return;this._updateState=8|this._updateState,null==t?this.removeAttribute(n):this.setAttribute(n,t),this._updateState=-9&this._updateState}}_attributeToProperty(t,e){if(8&this._updateState)return;const i=this.constructor,s=i._attributeToPropertyMap.get(t);if(void 0!==s){const t=i.getPropertyOptions(s);this._updateState=16|this._updateState,this[s]=i._propertyValueFromAttribute(e,t),this._updateState=-17&this._updateState}}requestUpdateInternal(t,e,i){let s=!0;if(void 0!==t){const n=this.constructor;i=i||n.getPropertyOptions(t),n._valueHasChanged(this[t],e,i.hasChanged)?(this._changedProperties.has(t)||this._changedProperties.set(t,e),!0!==i.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(t,i))):s=!1}!this._hasRequestedUpdate&&s&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(t,e){return this.requestUpdateInternal(t,e),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(t){}const t=this.performUpdate();return null!=t&&await t,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){if(!this._hasRequestedUpdate)return;this._instanceProperties&&this._applyInstanceProperties();let t=!1;const e=this._changedProperties;try{t=this.shouldUpdate(e),t?this.update(e):this._markUpdated()}catch(e){throw t=!1,this._markUpdated(),e}t&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(e)),this.updated(e))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(t){return!0}update(t){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach(((t,e)=>this._propertyToAttribute(e,this[e],t))),this._reflectingProperties=void 0),this._markUpdated()}updated(t){}firstUpdated(t){}}H.finalized=!0;const J=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?Object.assign(Object.assign({},e),{finisher(i){i.createProperty(e.key,t)}}):{kind:"field",key:Symbol(),placement:"own",descriptor:{},initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function K(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):J(t,e)}function Y(t){return K({attribute:!1,hasChanged:null==t?void 0:t.hasChanged})}const Q=(t,e,i)=>{Object.defineProperty(e,i,t)},X=(t,e)=>({kind:"method",placement:"prototype",key:e.key,descriptor:t}),Z=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,tt=Symbol();class et{constructor(t,e){if(e!==tt)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(Z?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const it=(t,...e)=>{const i=e.reduce(((e,i,s)=>e+(t=>{if(t instanceof et)return t.cssText;if("number"==typeof t)return t;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[s+1]),t[0]);return new et(i,tt)};(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");const st={};class nt extends H{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const t=this.getStyles();if(Array.isArray(t)){const e=(t,i)=>t.reduceRight(((t,i)=>Array.isArray(i)?e(i,t):(t.add(i),t)),i),i=e(t,new Set),s=[];i.forEach((t=>s.unshift(t))),this._styles=s}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!Z){const e=Array.prototype.slice.call(t.cssRules).reduce(((t,e)=>t+e.cssText),"");return new et(String(e),tt)}return t}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?Z?this.renderRoot.adoptedStyleSheets=t.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map((t=>t.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){const e=this.render();super.update(t),e!==st&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)})))}render(){return st}}function ot(){return document.querySelector("hc-main")?document.querySelector("hc-main").hass:document.querySelector("home-assistant")?document.querySelector("home-assistant").hass:void 0}nt.finalized=!0,nt.render=(t,e,s)=>{if(!s||"object"!=typeof s||!s.scopeName)throw new Error("The `scopeName` option is required.");const n=s.scopeName,o=U.has(e),r=R&&11===e.nodeType&&!!e.host,a=r&&!W.has(n),l=a?document.createDocumentFragment():e;if(((t,e,s)=>{let n=U.get(e);void 0===n&&(i(e,e.firstChild),U.set(e,n=new x(Object.assign({templateFactory:A},s))),n.appendInto(e)),n.setValue(t),n.commit()})(t,l,Object.assign({templateFactory:D(n)},s)),a){const t=U.get(l);U.delete(l);const s=t.value instanceof m?t.value.template:void 0;G(n,l,s),i(e,e.firstChild),e.appendChild(l),U.set(e,t)}!o&&r&&window.ShadyCSS.styleElement(e.host)};const rt="lovelace-player-device-id";function at(){if(!localStorage[rt]){const t=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);window.fully&&"function"==typeof fully.getDeviceId?localStorage[rt]=fully.getDeviceId():localStorage[rt]=`${t()}${t()}-${t()}${t()}`}return localStorage[rt]}let lt=at();const ct=new URLSearchParams(window.location.search);var dt;function ht(t){return!!String(t).includes("{%")||(!!String(t).includes("{{")||void 0)}ct.get("deviceID")&&null!==(dt=ct.get("deviceID"))&&("clear"===dt?localStorage.removeItem(rt):localStorage[rt]=dt,lt=at()),window.cardMod_template_cache=window.cardMod_template_cache||{};const ut=window.cardMod_template_cache;async function pt(t,e,i){const s=ot().connection,n=JSON.stringify([e,i]);let o=ut[n];o?(o.callbacks.has(t)||ft(t),t(o.value),o.callbacks.add(t)):(ft(t),t(""),i=Object.assign({user:ot().user.name,browser:lt,hash:location.hash.substr(1)||""},i),ut[n]=o={template:e,variables:i,value:"",callbacks:new Set([t]),unsubscribe:s.subscribeMessage((t=>function(t,e){const i=ut[t];i&&(i.value=e.result,i.callbacks.forEach((t=>t(e.result))))}(n,t)),{type:"render_template",template:e,variables:i})})}async function ft(t){let e;for(const[i,s]of Object.entries(ut))if(s.callbacks.has(t)){s.callbacks.delete(t),0==s.callbacks.size&&(e=s.unsubscribe,delete ut[i]);break}e&&await(await e)()}var gt;function _t(t,e){if("string"==typeof e&&"string"==typeof t&&(t.startsWith("/")&&t.endsWith("/")||-1!==t.indexOf("*"))){return t.startsWith("/")||(t=`/^${t=t.replace(/\./g,".").replace(/\*/g,".*")}$/`),new RegExp(t.slice(1,-1)).test(e)}if("string"==typeof t){if(t.startsWith("<="))return parseFloat(e)<=parseFloat(t.substr(2));if(t.startsWith(">="))return parseFloat(e)>=parseFloat(t.substr(2));if(t.startsWith("<"))return parseFloat(e)"))return parseFloat(e)>parseFloat(t.substr(1));if(t.startsWith("!"))return parseFloat(e)!=parseFloat(t.substr(1));if(t.startsWith("="))return parseFloat(e)==parseFloat(t.substr(1))}return t===e}window.autoEntities_cache=null!==(gt=window.autoEntities_cache)&&void 0!==gt?gt:{};const vt=window.autoEntities_cache;async function mt(t){var e;return vt.areas=null!==(e=vt.areas)&&void 0!==e?e:await t.callWS({type:"config/area_registry/list"}),vt.areas}async function yt(t){var e;return vt.devices=null!==(e=vt.devices)&&void 0!==e?e:await t.callWS({type:"config/device_registry/list"}),vt.devices}async function bt(t){var e;return vt.entities=null!==(e=vt.entities)&&void 0!==e?e:await t.callWS({type:"config/entity_registry/list"}),vt.entities}const wt={options:async()=>!0,sort:async()=>!0,domain:async(t,e,i)=>_t(e,i.entity_id.split(".")[0]),entity_id:async(t,e,i)=>_t(e,i.entity_id),state:async(t,e,i)=>_t(e,i.state),name:async(t,e,i)=>{var s;return _t(e,null===(s=i.attributes)||void 0===s?void 0:s.friendly_name)},group:async(t,e,i)=>{var s,n,o;return null===(o=null===(n=null===(s=t.states[e])||void 0===s?void 0:s.attributes)||void 0===n?void 0:n.entity_id)||void 0===o?void 0:o.includes(i.entity_id)},attributes:async(t,e,i)=>{for(const[t,s]of Object.entries(e)){let e=t.split(" ")[0],n=i.attributes;for(const t of e.split(":"))n=n?n[t]:void 0;if(void 0===n||!_t(s,n))return!1}return!0},not:async(t,e,i)=>!await St(t,e,i.entity_id),or:async(t,e,i)=>{for(const s of e)if(await St(t,s,i.entity_id))return!0;return!1},device:async(t,e,i)=>{const s=(await bt(t)).find((t=>t.entity_id===i.entity_id));if(!s)return!1;const n=(await yt(t)).find((t=>t.id===s.device_id));return!!n&&(_t(e,n.name_by_user)||_t(e,n.name))},area:async(t,e,i)=>{const s=(await bt(t)).find((t=>t.entity_id===i.entity_id));if(!s)return!1;let n=(await mt(t)).find((t=>t.area_id===s.area_id));if(n)return _t(e,n.name);const o=(await yt(t)).find((t=>t.id===s.device_id));return!!o&&(n=(await mt(t)).find((t=>t.area_id===o.area_id)),!!n&&_t(e,n.name))},last_changed:async(t,e,i)=>_t(e,((new Date).getTime()-new Date(i.last_changed).getTime())/6e4),last_updated:async(t,e,i)=>_t(e,((new Date).getTime()-new Date(i.last_updated).getTime())/6e4),last_triggered:async(t,e,i)=>{if(null==i.attributes.last_triggered)return!1;return _t(e,((new Date).getTime()-new Date(i.attributes.last_triggered).getTime())/6e4)}};async function St(t,e,i){var s;if(!t.states[i])return!1;for(let[n,o]of Object.entries(e))if(n=n.trim().split(" ")[0].trim(),!await(null===(s=wt[n])||void 0===s?void 0:s.call(wt,t,o,t.states[i])))return!1;return!0}function Ct(t,e,i){var s,n,o,r;const[a,l]=i.reverse?[-1,1]:[1,-1];return i.ignore_case&&(t=null!==(n=null===(s=null==t?void 0:t.toLowerCase)||void 0===s?void 0:s.call(t))&&void 0!==n?n:t,e=null!==(r=null===(o=null==e?void 0:e.toLowerCase)||void 0===o?void 0:o.call(e))&&void 0!==r?r:e),i.numeric&&(isNaN(parseFloat(t))&&isNaN(parseFloat(e))||(t=isNaN(parseFloat(t))?void 0:parseFloat(t),e=isNaN(parseFloat(e))?void 0:parseFloat(e))),void 0===t&&void 0===e?0:void 0===t?a:void 0===e?l:(i.reverse?-1:1)*String(t).localeCompare(String(e),void 0,i)}const Ot={none:()=>0,domain:(t,e,i)=>{var s,n;return Ct(null===(s=null==t?void 0:t.entity_id)||void 0===s?void 0:s.split(".")[0],null===(n=null==e?void 0:e.entity_id)||void 0===n?void 0:n.split(".")[0],i)},entity_id:(t,e,i)=>Ct(null==t?void 0:t.entity_id,null==e?void 0:e.entity_id,i),friendly_name:(t,e,i)=>{var s,n,o,r;return Ct((null===(s=null==t?void 0:t.attributes)||void 0===s?void 0:s.friendly_name)||(null===(n=null==t?void 0:t.entity_id)||void 0===n?void 0:n.split(".")[1]),(null===(o=null==e?void 0:e.attributes)||void 0===o?void 0:o.friendly_name)||(null===(r=null==e?void 0:e.entity_id)||void 0===r?void 0:r.split(".")[1]),i)},name:(t,e,i)=>{var s,n,o,r;return Ct((null===(s=null==t?void 0:t.attributes)||void 0===s?void 0:s.friendly_name)||(null===(n=null==t?void 0:t.entity_id)||void 0===n?void 0:n.split(".")[1]),(null===(o=null==e?void 0:e.attributes)||void 0===o?void 0:o.friendly_name)||(null===(r=null==e?void 0:e.entity_id)||void 0===r?void 0:r.split(".")[1]),i)},state:(t,e,i)=>Ct(null==t?void 0:t.state,null==e?void 0:e.state,i),attribute:(t,e,i)=>{var s;const[n,o]=(null==i?void 0:i.reverse)?[-1,1]:[1,-1];let r=null==t?void 0:t.attributes,a=null==e?void 0:e.attributes;for(const t of null===(s=null==i?void 0:i.attribute)||void 0===s?void 0:s.split(":")){if(void 0===r&&void 0===a)return 0;if(void 0===r)return n;if(void 0===a)return o;[r,a]=[r[t],a[t]]}return Ct(r,a,i)},last_changed:(t,e,i)=>{const[s,n]=(null==i?void 0:i.reverse)?[-1,1]:[1,-1];return null==(null==t?void 0:t.last_changed)&&null==(null==e?void 0:e.last_changed)?0:null==(null==t?void 0:t.last_changed)?s:null==(null==e?void 0:e.last_changed)?n:(i.numeric=!0,Ct(new Date(null==t?void 0:t.last_changed).getTime(),new Date(null==e?void 0:e.last_changed).getTime(),i))},last_updated:(t,e,i)=>{const[s,n]=(null==i?void 0:i.reverse)?[-1,1]:[1,-1];return null==(null==t?void 0:t.last_updated)&&null==(null==e?void 0:e.last_updated)?0:null==(null==t?void 0:t.last_updated)?s:null==(null==e?void 0:e.last_updated)?n:(i.numeric=!0,Ct(new Date(null==t?void 0:t.last_updated).getTime(),new Date(null==e?void 0:e.last_updated).getTime(),i))},last_triggered:(t,e,i)=>{var s,n,o,r,a,l;const[c,d]=(null==i?void 0:i.reverse)?[-1,1]:[1,-1];return null==(null===(s=null==t?void 0:t.attributes)||void 0===s?void 0:s.last_triggered)&&null==(null===(n=null==e?void 0:e.attributes)||void 0===n?void 0:n.last_triggered)?0:null==(null===(o=null==t?void 0:t.attributes)||void 0===o?void 0:o.last_triggered)?c:null==(null===(r=null==e?void 0:e.attributes)||void 0===r?void 0:r.last_triggered)?d:(i.numeric=!0,Ct(new Date(null===(a=null==t?void 0:t.attributes)||void 0===a?void 0:a.last_triggered).getTime(),new Date(null===(l=null==e?void 0:e.attributes)||void 0===l?void 0:l.last_triggered).getTime(),i))}};function Et(t,e){return function(i,s){var n,o;return null!==(o=null===(n=Ot[e.method])||void 0===n?void 0:n.call(Ot,t.states[i.entity],t.states[s.entity],e))&&void 0!==o?o:0}}var xt="1.9.1";const Pt=["domain","entity_id","state","name","group","device","area","last_changed","last_updated","last_triggered"],jt=["none","domain","entity_id","friendly_name","state","last_changed","last_updated","last_triggered"];class $t extends nt{constructor(){super(...arguments),this._selectedTab=0,this._cardGUIMode=!0,this._cardGUIModeAvailable=!0}setConfig(t){this._config=t}_handleSwitchTab(t){this._selectedTab=parseInt(t.detail.index,10)}_addFilterGroup(){var t;if(!this._config)return;const e=[...null===(t=this._config.filter)||void 0===t?void 0:t.include];e.push({domain:""});const i=Object.assign(Object.assign({},this._config.filter),{include:e});this._config=Object.assign(Object.assign({},this._config),{filter:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_deleteFilterGroup(t){var e;if(!this._config)return;const i=[...null===(e=this._config.filter)||void 0===e?void 0:e.include];i.splice(t,1);const s=Object.assign(Object.assign({},this._config.filter),{include:i});this._config=Object.assign(Object.assign({},this._config),{filter:s}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_moveFilterGroup(t,e){var i;if(!this._config)return;const s=[...null===(i=this._config.filter)||void 0===i?void 0:i.include];[s[t],s[t+e]]=[s[t+e],s[t]];const n=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:n}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_addSpecialEntry(){var t;if(!this._config)return;const e=[...null===(t=this._config.filter)||void 0===t?void 0:t.include];e.push({type:""});const i=Object.assign(Object.assign({},this._config.filter),{include:e});this._config=Object.assign(Object.assign({},this._config),{filter:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}async _changeSpecialEntry(t,e){var i;if(!this._config)return;const s=[...null===(i=this._config.filter)||void 0===i?void 0:i.include];s[t]=e.detail.value;const n=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:n}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}async _changeGroupOptions(t,e){var i;if(!this._config)return;const s=e.detail.value,n=[...null===(i=this._config.filter)||void 0===i?void 0:i.include];n[t]=Object.assign(Object.assign({},n[t]),{options:s});const o=Object.assign(Object.assign({},this._config.filter),{include:n});this._config=Object.assign(Object.assign({},this._config),{filter:o}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_addFilter(t){var e;if(!this._config)return;const i=Pt.find((e=>void 0===this._config.filter.include[t][e]));if(void 0===i)return;const s=[...null===(e=this._config.filter)||void 0===e?void 0:e.include];s[t]=Object.assign(Object.assign({},s[t]),{[i]:""});const n=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:n}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_removeFilter(t,e){var i;if(!this._config)return;const s=[...null===(i=this._config.filter)||void 0===i?void 0:i.include],n=Object.assign({},s[t]);if(delete n[e],0===Object.keys(n).length)return this._deleteFilterGroup(t);s[t]=n;const o=Object.assign(Object.assign({},this._config.filter),{include:s});this._config=Object.assign(Object.assign({},this._config),{filter:o}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeFilterKey(t,e,i){var s;if(!this._config)return;const n=Pt[i.target.selected];if(void 0===n||n===e)return;const o=[...null===(s=this._config.filter)||void 0===s?void 0:s.include],r=Object.assign({},o[t]);if(void 0===r[e])return;r[n]=r[e],delete r[e],o[t]=r;const a=Object.assign(Object.assign({},this._config.filter),{include:o});this._config=Object.assign(Object.assign({},this._config),{filter:a}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeFilterValue(t,e,i){var s;if(!this._config)return;const n=[...null===(s=this._config.filter)||void 0===s?void 0:s.include],o=Object.assign({},n[t]);o[e]=i.target.value,n[t]=o;const r=Object.assign(Object.assign({},this._config.filter),{include:n});this._config=Object.assign(Object.assign({},this._config),{filter:r}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeSortMethod(t){if(!this._config)return;const e=jt[t.target.selected],i=Object.assign(Object.assign({},this._config.sort),{method:e});this._config=Object.assign(Object.assign({},this._config),{sort:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_sortOptionToggle(t,e){if(!this._config)return;const i=Object.assign({},this._config.sort);i[t]=e.target.checked,this._config=Object.assign(Object.assign({},this._config),{sort:i}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_showEmptyToggle(){if(!this._config)return;const t=!1===this._config.show_empty;this._config=Object.assign(Object.assign({},this._config),{show_empty:t}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_changeCardParam(t){if(!this._config)return;const e=""===t.target.value||"entities"===t.target.value?void 0:t.target.value;this._config=Object.assign(Object.assign({},this._config),{card_param:e}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_getCardConfig(){const t=Object.assign({},this._config.card);return t[this._config.card_param||"entities"]=[],t}_handleCardPicked(t){if(t.stopPropagation(),!this._config)return;const e=Object.assign({},t.detail.config);delete e.entities,this._config=Object.assign(Object.assign({},this._config),{card:e}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_handleCardConfigChanged(t){if(t.stopPropagation(),!this._config)return;const e=Object.assign({},t.detail.config);delete e[this._config.card_param||"entities"],this._config=Object.assign(Object.assign({},this._config),{card:e}),this._cardGUIModeAvailable=t.detail.guiModeAvailable,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_deleteCard(t){this._config&&(this._config=Object.assign({},this._config),delete this._config.card,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_toggleCardMode(t){var e;null===(e=this._cardEditorEl)||void 0===e||e.toggleMode()}_cardGUIModeChanged(t){t.stopPropagation(),this._cardGUIMode=t.detail.guiMode,this._cardGUIModeAvailable=t.detail.guiModeAvailable}render(){return this.hass&&this._config?V`
- `:V``}_renderFilterEditor(){return V` + `:V``}_renderFilterEditor(){var t;return(null===(t=this._config.filter)||void 0===t?void 0:t.template)||this._config.entities?V` +
+

+ Your filter method is not handled by the GUI editor. +

+

Please switch to the CODE EDITOR to access all options.

+
+ `:V` ${this._config.filter.include.map(((t,e)=>V`
@@ -38,15 +45,15 @@ function t(t,e,i,s){var n,o=arguments.length,r=o<3?e:null===s?s=Object.getOwnPro
${void 0===t.type?V` ${Object.entries(t).map((([t,i],s)=>V` - ${jt.includes(t)?V` + ${Pt.includes(t)?V`
this._changeFilterKey(e,t,i)} > - ${jt.map((t=>V` ${t} `))} + ${Pt.map((t=>V` ${t} `))} Add non-filter entry - `}_renderSortEditor(){var t,e,i,s;return V` + `}_renderSortEditor(){var t,e,i,s,n;return V`
- ${(null===(t=this._config.sort)||void 0===t?void 0:t.method)&&!Pt.includes(this._config.sort.method)?V`

+ ${(null===(t=this._config.sort)||void 0===t?void 0:t.method)&&!jt.includes(this._config.sort.method)?V`

Your sort method is not handled by the GUI editor.

Please switch to the CODE EDITOR to access all options.

`:V` Method: - ${Pt.map((t=>V` ${t} `))} + ${jt.map((t=>V` ${t} `))} - - this._sortOptionToggle("reverse",t)} - > - +

+ + this._sortOptionToggle("reverse",t)} + > + +

+

+ + this._sortOptionToggle("numeric",t)} + > + +

`}
`}_renderCardEditor(){var t;return V` @@ -191,4 +207,4 @@ function t(t,e,i,s){var n,o=arguments.length,r=o<3?e:null===s?s=Object.getOwnPro .gui-mode-button { margin-right: auto; } - `]}}function Nt(t,e){if(t===e)return!0;if(typeof t!=typeof e)return!1;if(!(t instanceof Object&&e instanceof Object))return!1;for(const i in t)if(t.hasOwnProperty(i)){if(!e.hasOwnProperty(i))return!1;if(t[i]!==e[i]){if("object"!=typeof t[i])return!1;if(!Nt(t[i],e[i]))return!1}}for(const i in e)if(e.hasOwnProperty(i)&&!t.hasOwnProperty(i))return!1;return!0}t([Y()],$t.prototype,"_config",void 0),t([K()],$t.prototype,"lovelace",void 0),t([K()],$t.prototype,"hass",void 0),t([Y()],$t.prototype,"_selectedTab",void 0),t([Y()],$t.prototype,"_cardGUIMode",void 0),t([Y()],$t.prototype,"_cardGUIModeAvailable",void 0),t([function(t,e){return(i,s)=>{const n={get(){return this.renderRoot.querySelector(t)},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof s?Symbol():`__${s}`;n.get=function(){return void 0===this[e]&&(this[e]=this.renderRoot.querySelector(t)),this[e]}}return void 0!==s?Q(n,i,s):X(n,i)}}("hui-card-element-editor")],$t.prototype,"_cardEditorEl",void 0),customElements.define("auto-entities-editor",$t),window.customCards=window.customCards||[],window.customCards.push({type:"auto-entities",name:"Auto Entities",preview:!1,description:"Entity Filter on Steroids. Auto Entities allows you to fill other cards with entities automatically, based on a number of attributes."});class Tt extends nt{constructor(){super(...arguments),this._updateCooldown={timer:void 0,rerun:!1},this._renderer=t=>{this._template="string"==typeof t?t.split(/[\s,]+/):t}}static getConfigElement(){return document.createElement("auto-entities-editor")}static getStubConfig(){return{card:{type:"entities"},filter:{include:[],exclude:[]}}}setConfig(t){var e,i;if(!t)throw new Error("No configuration.");if(!(null===(e=t.card)||void 0===e?void 0:e.type))throw new Error("No card type specified.");if(!t.filter&&!t.entities)throw new Error("No filters specified.");t=JSON.parse(JSON.stringify(t)),this._config=t,(null===(i=this._config.filter)||void 0===i?void 0:i.template)&&ht(this._config.filter.template)&&pt(this._renderer,this._config.filter.template,{config:t}),this._cardBuilt=new Promise((t=>this._cardBuiltResolve=t)),queueMicrotask((()=>this.update_all()))}connectedCallback(){var t,e;super.connectedCallback(),(null===(e=null===(t=this._config)||void 0===t?void 0:t.filter)||void 0===e?void 0:e.template)&&ht(this._config.filter.template)&&pt(this._renderer,this._config.filter.template,{config:this._config})}disconnectedCallback(){super.disconnectedCallback(),ft(this._renderer)}async update_all(){if(this.card&&(this.card.hass=this.hass),this._updateCooldown.timer)return void(this._updateCooldown.rerun=!0);this._updateCooldown.rerun=!1,this._updateCooldown.timer=window.setTimeout((()=>{this._updateCooldown.timer=void 0,this._updateCooldown.rerun&&this.update_all()}),500);const t=await this.update_entities();this.update_card(t)}async update_card(t){var e,i;if(this._entities&&Nt(t,this._entities)&&Nt(this._cardConfig,this._config.card))return;const s=(null===(e=this._cardConfig)||void 0===e?void 0:e.type)!==this._config.card.type;this._entities=t,this._cardConfig=JSON.parse(JSON.stringify(this._config.card));const n=Object.assign({[this._config.card_param||"entities"]:t},this._config.card);if(!this.card||s){const t=await window.loadCardHelpers();this.card=await t.createCardElement(n)}else this.card.setConfig(n);null===(i=this._cardBuiltResolve)||void 0===i||i.call(this),this.card.hass=this.hass;const o=0===t.length&&!1===this._config.show_empty;this.style.display=o?"none":null,this.style.margin=o?"0":null,this.card.requestUpdate&&(await this.updateComplete,this.card.requestUpdate())}async update_entities(){var t,e,i;const s=t=>t?"string"==typeof t?{entity:t.trim()}:t:null;let n=[...(null===(e=null===(t=this._config)||void 0===t?void 0:t.entities)||void 0===e?void 0:e.map(s))||[]];if(!this.hass||!this._config.filter)return n;if(this._template&&(n=n.concat(this._template.map(s))),n=n.filter(Boolean),this._config.filter.include){const t=Object.keys(this.hass.states).map(s);for(const e of this._config.filter.include){if(e.type){n.push(e);continue}let i=[];for(const s of t)await St(this.hass,e,s.entity)&&i.push(JSON.parse(JSON.stringify(Object.assign(Object.assign({},s),e.options)).replace(/this.entity_id/g,s.entity)));e.sort&&(i=i.sort(Et(this.hass,e.sort))),n=n.concat(i)}}if(this._config.filter.exclude)for(const t of this._config.filter.exclude){const e=[];for(const i of n)void 0!==i.entity&&await St(this.hass,t,i.entity)||e.push(i);n=e}if(this._config.sort&&(n=n.sort(Et(this.hass,this._config.sort)),this._config.sort.count)){const t=null!==(i=this._config.sort.first)&&void 0!==i?i:0;n=n.slice(t,t+this._config.sort.count)}if(this._config.unique){let t=[];for(const e of n)"entity"===this._config.unique&&t.some((t=>t.entity===e.entity))||t.some((t=>Nt(t,e)))||t.push(e);n=t}return n}async updated(t){(t.has("_template")||t.has("hass")&&this.hass)&&queueMicrotask((()=>this.update_all()))}createRenderRoot(){return this}render(){return V`${this.card}`}async getCardSize(){var t,e;let i=0;return await this._cardBuilt,this.card&&this.card.getCardSize&&(i=await this.card.getCardSize()),1===i&&(null===(t=this._entities)||void 0===t?void 0:t.length)&&(i=this._entities.length),0===i&&(null===(e=this._config.filter)||void 0===e?void 0:e.include)&&(i=Object.keys(this._config.filter.include).length),i||5}}t([K()],Tt.prototype,"_config",void 0),t([K()],Tt.prototype,"hass",void 0),t([K()],Tt.prototype,"card",void 0),t([K()],Tt.prototype,"_template",void 0),customElements.get("auto-entities")||(customElements.define("auto-entities",Tt),console.info(`%cAUTO-ENTITIES ${xt} IS INSTALLED`,"color: green; font-weight: bold","")); + `]}}function Nt(t,e){if(t===e)return!0;if(typeof t!=typeof e)return!1;if(!(t instanceof Object&&e instanceof Object))return!1;for(const i in t)if(t.hasOwnProperty(i)){if(!e.hasOwnProperty(i))return!1;if(t[i]!==e[i]){if("object"!=typeof t[i])return!1;if(!Nt(t[i],e[i]))return!1}}for(const i in e)if(e.hasOwnProperty(i)&&!t.hasOwnProperty(i))return!1;return!0}t([Y()],$t.prototype,"_config",void 0),t([K()],$t.prototype,"lovelace",void 0),t([K()],$t.prototype,"hass",void 0),t([Y()],$t.prototype,"_selectedTab",void 0),t([Y()],$t.prototype,"_cardGUIMode",void 0),t([Y()],$t.prototype,"_cardGUIModeAvailable",void 0),t([function(t,e){return(i,s)=>{const n={get(){return this.renderRoot.querySelector(t)},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof s?Symbol():`__${s}`;n.get=function(){return void 0===this[e]&&(this[e]=this.renderRoot.querySelector(t)),this[e]}}return void 0!==s?Q(n,i,s):X(n,i)}}("hui-card-element-editor")],$t.prototype,"_cardEditorEl",void 0),customElements.define("auto-entities-editor",$t),window.customCards=window.customCards||[],window.customCards.push({type:"auto-entities",name:"Auto Entities",preview:!1,description:"Entity Filter on Steroids. Auto Entities allows you to fill other cards with entities automatically, based on a number of attributes."}),window.queueMicrotask=window.queueMicrotask||(t=>window.setTimeout(t,1));class Tt extends nt{constructor(){super(...arguments),this._updateCooldown={timer:void 0,rerun:!1},this._renderer=t=>{this._template="string"==typeof t?t.split(/[\s,]+/):t}}static getConfigElement(){return document.createElement("auto-entities-editor")}static getStubConfig(){return{card:{type:"entities"},filter:{include:[],exclude:[]}}}setConfig(t){var e,i;if(!t)throw new Error("No configuration.");if(!(null===(e=t.card)||void 0===e?void 0:e.type))throw new Error("No card type specified.");if(!t.filter&&!t.entities)throw new Error("No filters specified.");t=JSON.parse(JSON.stringify(t)),this._config=t,(null===(i=this._config.filter)||void 0===i?void 0:i.template)&&ht(this._config.filter.template)&&pt(this._renderer,this._config.filter.template,{config:t}),this._cardBuilt=new Promise((t=>this._cardBuiltResolve=t)),queueMicrotask((()=>this.update_all()))}connectedCallback(){var t,e;super.connectedCallback(),(null===(e=null===(t=this._config)||void 0===t?void 0:t.filter)||void 0===e?void 0:e.template)&&ht(this._config.filter.template)&&pt(this._renderer,this._config.filter.template,{config:this._config})}disconnectedCallback(){super.disconnectedCallback(),ft(this._renderer)}async update_all(){if(this.card&&(this.card.hass=this.hass),this._updateCooldown.timer)return void(this._updateCooldown.rerun=!0);this._updateCooldown.rerun=!1,this._updateCooldown.timer=window.setTimeout((()=>{this._updateCooldown.timer=void 0,this._updateCooldown.rerun&&this.update_all()}),500);const t=await this.update_entities();this.update_card(t)}async update_card(t){var e,i,s;if(this._entities&&Nt(t,this._entities)&&Nt(this._cardConfig,this._config.card))return;const n=(null===(e=this._cardConfig)||void 0===e?void 0:e.type)!==this._config.card.type;this._entities=t,this._cardConfig=JSON.parse(JSON.stringify(this._config.card));const o=Object.assign({[this._config.card_param||"entities"]:t},this._config.card);if(!this.card||n){const t=await window.loadCardHelpers(),e=console.error;let s=!1;if(console.error=(...t)=>{var i,n,o,r,a,l;3===t.length&&t[2].message&&((null===(n=(i=t[2].message).startsWith)||void 0===n?void 0:n.call(i,"Entities"))||(null===(r=(o=t[2].message).startsWith)||void 0===r?void 0:r.call(o,"Either entities"))||(null===(l=(a=t[2].message).endsWith)||void 0===l?void 0:l.call(a,"entity")))?s=!0:e(...t)},this.card=await t.createCardElement(o),console.error=e,s)return this.card=void 0,this._entities=void 0,this._cardConfig=void 0,void(null===(i=this._cardBuiltResolve)||void 0===i||i.call(this))}else this.card.setConfig(o);null===(s=this._cardBuiltResolve)||void 0===s||s.call(this),this.card.hass=this.hass;const r=0===t.length&&!1===this._config.show_empty;this.style.display=r?"none":null,this.style.margin=r?"0":null,this.card.requestUpdate&&(await this.updateComplete,this.card.requestUpdate())}async update_entities(){var t,e,i,s,n,o;const r=t=>t?"string"==typeof t?{entity:t.trim()}:t:null;let a=[...(null===(e=null===(t=this._config)||void 0===t?void 0:t.entities)||void 0===e?void 0:e.map(r))||[]];if(!this.hass)return a;if(this._template&&(a=a.concat(this._template.map(r))),a=a.filter(Boolean),null===(i=this._config.filter)||void 0===i?void 0:i.include){const t=Object.keys(this.hass.states).map(r);for(const e of this._config.filter.include){if(e.type){a.push(e);continue}let i=[];for(const s of t)await St(this.hass,e,s.entity)&&i.push(JSON.parse(JSON.stringify(Object.assign(Object.assign({},s),e.options)).replace(/this.entity_id/g,s.entity)));if(e.sort&&(i=i.sort(Et(this.hass,e.sort)),e.sort.count)){const t=null!==(s=e.sort.first)&&void 0!==s?s:0;i=i.slice(t,t+e.sort.count)}a=a.concat(i)}}if(null===(n=this._config.filter)||void 0===n?void 0:n.exclude)for(const t of this._config.filter.exclude){const e=[];for(const i of a)void 0!==i.entity&&await St(this.hass,t,i.entity)||e.push(i);a=e}if(this._config.sort&&(a=a.sort(Et(this.hass,this._config.sort)),this._config.sort.count)){const t=null!==(o=this._config.sort.first)&&void 0!==o?o:0;a=a.slice(t,t+this._config.sort.count)}if(this._config.unique){let t=[];for(const e of a)"entity"===this._config.unique&&e.entity&&t.some((t=>t.entity===e.entity))||t.some((t=>Nt(t,e)))||t.push(e);a=t}return a}async updated(t){(t.has("_template")||t.has("hass")&&this.hass)&&queueMicrotask((()=>this.update_all()))}createRenderRoot(){return this}render(){return V`${this.card}`}async getCardSize(){var t,e;let i=0;return await this._cardBuilt,this.card&&this.card.getCardSize&&(i=await this.card.getCardSize()),1===i&&(null===(t=this._entities)||void 0===t?void 0:t.length)&&(i=this._entities.length),0===i&&(null===(e=this._config.filter)||void 0===e?void 0:e.include)&&(i=Object.keys(this._config.filter.include).length),i||5}}t([K()],Tt.prototype,"_config",void 0),t([K()],Tt.prototype,"hass",void 0),t([K()],Tt.prototype,"card",void 0),t([K()],Tt.prototype,"_template",void 0),customElements.get("auto-entities")||(customElements.define("auto-entities",Tt),console.info(`%cAUTO-ENTITIES ${xt} IS INSTALLED`,"color: green; font-weight: bold","")); diff --git a/www/community/lovelace-auto-entities/auto-entities.js.gz b/www/community/lovelace-auto-entities/auto-entities.js.gz index 821591ac15c0fd929818e58234a547275f2506e4..dd269cd535023046363e6961a273b9cc1e7a1bc1 100644 GIT binary patch literal 13338 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 diff --git a/www/community/lovelace-valetudo-map-card/valetudo-map-card.js b/www/community/lovelace-valetudo-map-card/valetudo-map-card.js index fb8d2ede..df232815 100644 --- a/www/community/lovelace-valetudo-map-card/valetudo-map-card.js +++ b/www/community/lovelace-valetudo-map-card/valetudo-map-card.js @@ -534,6 +534,9 @@ class ValetudoMapCard extends HTMLElement { if (this._config.virtual_wall_width === undefined) this._config.virtual_wall_width = 1; if (this._config.path_width === undefined) this._config.path_width = 1; + // Padding settings + if (this._config.left_padding === undefined) this._config.left_padding = 0; + // Scale settings if (this._config.map_scale === undefined) this._config.map_scale = 1; if (this._config.icon_scale === undefined) this._config.icon_scale = 1; @@ -632,7 +635,7 @@ class ValetudoMapCard extends HTMLElement { if(this.isPollingMap === false ) { this.isPollingMap = true; - const response = await fetch(url); + const response = await this._hass.fetchWithAuth(url); let mapData; if(!response.ok) { @@ -748,6 +751,7 @@ class ValetudoMapCard extends HTMLElement { height: ${containerHeight}px; padding-top: ${containerMinHeightPadding}px; padding-bottom: ${containerMinHeightPadding}px; + padding-left: ${this._config.left_padding}px; overflow: hidden; } #lovelaceValetudoCard { diff --git a/www/community/lovelace-valetudo-map-card/valetudo-map-card.js.gz b/www/community/lovelace-valetudo-map-card/valetudo-map-card.js.gz index caa3aed674ff5157374c01612efe90e85ef24b8c..286d0ea6989e13fb13ced62ef4f1b1d92b6d67c5 100644 GIT binary patch delta 14664 zcmV-OIk(2Ijset;0SF(92nf+2HDR#``wf4_mp8%-n`=6Nr>KE9SkYC@=mo{%M9KwS zj9uN7JfE<#9i`M#%4F->m7vsODrG*h<=+}~wPUI)+sTz&igxZO-fI5Lmo*yoz4j8o zs%w2ERK4I*L{}dnSlPn5s?d#Py(n%Su578S_zLJ{+Yp<}>Ux@~j)nE|s=<>trRjgP zJ|FpY(~EZ8aU)g|MU4x-nyUPVb8zGwITc$b>>V99T6;>u+js3 z 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 K_mK`n9RUDTJ#I7r delta 14663 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 JV={~!0RXQPpsWA@ diff --git a/www/community/numberbox-card/numberbox-card.js b/www/community/numberbox-card/numberbox-card.js index 1901168a..dfaa199c 100644 --- a/www/community/numberbox-card/numberbox-card.js +++ b/www/community/numberbox-card/numberbox-card.js @@ -1,6 +1,6 @@ ((LitElement) => { -console.info('NUMBERBOX_CARD 2.7'); +console.info('NUMBERBOX_CARD 2.9'); const html = LitElement.prototype.html; const css = LitElement.prototype.css; class NumberBox extends LitElement { @@ -98,8 +98,15 @@ publishNum(dhis){ niceNum(){ let fix=0; let v=this.pending; - if( v === false ){ v=Number(this.stateObj.state); if(isNaN(v)){return '?';}} - const stp=Number(this.stateObj.attributes.step); + if( v === false ){ + v=this.stateObj.state; + if(v=='unavailable' || ( v=='unknown' && this.config.initial === undefined ) ){return '?';} + v=Number(v); + if(isNaN(v) && this.config.initial !== undefined){ + v=Number(this.config.initial); + } + } + const stp=Number(this.stateObj.attributes.step) || 1; if( Math.round(stp) != stp ){ fix=stp.toString().split(".")[1].length || 1;} fix = v.toFixed(fix); const u=this.config.unit; @@ -214,6 +221,7 @@ setConfig(config) { icon_plus: config.icon_plus, icon_minus: config.icon_minus, delay: config.delay, + initial: config.initial, secondary_info: config.secondary_info, }; } @@ -226,7 +234,7 @@ set hass(hass) { } shouldUpdate(changedProps) { - if( changedProps.has('stateObj') || changedProps.has('pending') ){return true;} + if( changedProps.has('config') || changedProps.has('stateObj') || changedProps.has('pending') ){return true;} } static getConfigElement() { @@ -294,6 +302,13 @@ render() { @change="${this.updVal}" allow-custom-entity > + + +
- - - +
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