diff --git a/.HA_VERSION b/.HA_VERSION index 8c53e4e..ddfcf48 100644 --- a/.HA_VERSION +++ b/.HA_VERSION @@ -1 +1 @@ -0.107.7 \ No newline at end of file +2021.7.4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c7f0389..903ed11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,31 @@ -# Git Ignore File - -# Do not ignore -!/.gitignore -!/.travis.yml - -# Logs -logs +# Generic ignores *.log +*.db +*.db-shm +*.db-wal +*.pyc +._* +__pycache__ -# Ignore Folders -.storage +# Specific directory ignores .cloud -.vscode -.Trash-* -deps/ +.storage +deps -# Ignored Files +# Specific file ignores secrets.yaml ip_bans.yaml known_devices.yaml -*.conf -*.db -*.uuid +.ios.conf +.uuid +alexa_media* -# Node-Red -node-red +# ESPHome ignores +esphome/secrets.yaml +esphome/**/ -# Operating System Trash Files -.DS_Store - -# Alexa Media -alexa_media.*.pickle +# Ignore files created by IDE's +.vscode -# Ring -.ring_cache.pickle \ No newline at end of file +# Specific keeps +!.gitkeep \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 24be4a7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -# -# Travis CI configuration -# -# Simply tests that Home Assistant configuration is valid. -# -# @link https://home-assistant.io/docs/ecosystem/backup/backup_github/#step-7-configuration-file-testing -# - -language: python - -python: - - "3.6" - -before_install: - - cp secrets_dummy.yaml secrets.yaml - - touch home-assistant_v2.db - - touch home-assistant.log - -install: - - pip3 install homeassistant - -script: -- hass --script check_config -c . -f \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7f74b9f..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019-2020 Jonathan Adams - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/assistants/alexa/areas/bathroom/climate.bathroom.yaml b/assistants/alexa/areas/bathroom/climate.bathroom.yaml deleted file mode 100644 index c63ab50..0000000 --- a/assistants/alexa/areas/bathroom/climate.bathroom.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -climate.bathroom: - name: Bathroom Radiator - description: Radiator in the Bathroom - display_categories: THERMOSTAT \ No newline at end of file diff --git a/assistants/alexa/areas/hallway/climate.hallway.yaml b/assistants/alexa/areas/hallway/climate.hallway.yaml deleted file mode 100644 index 25d6aa4..0000000 --- a/assistants/alexa/areas/hallway/climate.hallway.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- - # Entity configuration for Amazon Alexa. - # https://www.nabucasa.com/config/amazon_alexa/ - - climate.hallway: - name: Hallway Radiator - description: Radiator in the Hallway - display_categories: THERMOSTAT \ No newline at end of file diff --git a/assistants/alexa/areas/kitchen/climate.kitchen.yaml b/assistants/alexa/areas/kitchen/climate.kitchen.yaml deleted file mode 100644 index d85b1fb..0000000 --- a/assistants/alexa/areas/kitchen/climate.kitchen.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -climate.kitchen: - name: Kitchen Radiator - description: Radiator in the Kitchen - display_categories: THERMOSTAT \ No newline at end of file diff --git a/assistants/alexa/areas/kitchen/light.kitchen_table.yaml b/assistants/alexa/areas/kitchen/light.kitchen_table.yaml deleted file mode 100644 index a918bc1..0000000 --- a/assistants/alexa/areas/kitchen/light.kitchen_table.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -light.kitchen_table: - name: Kitchen Table - description: Table light in the Kitchen - display_categories: LIGHT \ No newline at end of file diff --git a/assistants/alexa/areas/living_room/climate.living_room.yaml b/assistants/alexa/areas/living_room/climate.living_room.yaml deleted file mode 100644 index fcaad90..0000000 --- a/assistants/alexa/areas/living_room/climate.living_room.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -climate.living_room: - name: Living Room Radiator - description: Radiator in the Living Room - display_categories: THERMOSTAT \ No newline at end of file diff --git a/assistants/alexa/areas/living_room/light.living_room_left_lamp.yaml b/assistants/alexa/areas/living_room/light.living_room_left_lamp.yaml deleted file mode 100644 index bf3a311..0000000 --- a/assistants/alexa/areas/living_room/light.living_room_left_lamp.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -light.living_room_left_lamp: - name: Living Room Left Lamp - description: Left Lamp in the Living Room - display_categories: LIGHT \ No newline at end of file diff --git a/assistants/alexa/areas/living_room/light.living_room_right_lamp.yaml b/assistants/alexa/areas/living_room/light.living_room_right_lamp.yaml deleted file mode 100644 index ec6ae92..0000000 --- a/assistants/alexa/areas/living_room/light.living_room_right_lamp.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -light.living_room_right_lamp: - name: Living Room Right Lamp - description: Right Lamp in the Living Room - display_categories: LIGHT \ No newline at end of file diff --git a/assistants/alexa/areas/living_room/light.living_room_tall_lamp.yaml b/assistants/alexa/areas/living_room/light.living_room_tall_lamp.yaml deleted file mode 100644 index 52b3602..0000000 --- a/assistants/alexa/areas/living_room/light.living_room_tall_lamp.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -light.living_room_tall_lamp: - name: Living Room Tall Lamp - description: Tall Lamp in the Living Room - display_categories: LIGHT \ No newline at end of file diff --git a/assistants/alexa/areas/master_bedroom/climate.master_bedroom.yaml b/assistants/alexa/areas/master_bedroom/climate.master_bedroom.yaml deleted file mode 100644 index 67fe9c5..0000000 --- a/assistants/alexa/areas/master_bedroom/climate.master_bedroom.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -climate.master_bedroom: - name: Bedroom Radiator - description: Radiator in the Master Bedroom - display_categories: THERMOSTAT \ No newline at end of file diff --git a/assistants/alexa/areas/master_bedroom/light.master_bedroom_jon_bedside_lamp.yaml b/assistants/alexa/areas/master_bedroom/light.master_bedroom_jon_bedside_lamp.yaml deleted file mode 100644 index 08606bc..0000000 --- a/assistants/alexa/areas/master_bedroom/light.master_bedroom_jon_bedside_lamp.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -light.master_bedroom_jon_bedside_lamp: - name: Bedroom Jon Bedside Lamp - description: Jon's Bedside Lamp in the Master Bedroom - display_categories: LIGHT \ No newline at end of file diff --git a/assistants/alexa/areas/master_bedroom/light.master_bedroom_lee_bedside_lamp.yaml b/assistants/alexa/areas/master_bedroom/light.master_bedroom_lee_bedside_lamp.yaml deleted file mode 100644 index 41ec5bc..0000000 --- a/assistants/alexa/areas/master_bedroom/light.master_bedroom_lee_bedside_lamp.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -light.master_bedroom_jon_bedside_lamp: - name: Bedroom Lee Bedside Lamp - description: Lee's Bedside Lamp in the Master Bedroom - display_categories: LIGHT \ No newline at end of file diff --git a/assistants/alexa/areas/office/climate.office.yaml b/assistants/alexa/areas/office/climate.office.yaml deleted file mode 100644 index 091b09b..0000000 --- a/assistants/alexa/areas/office/climate.office.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -climate.office: - name: Office Radiator - description: Radiator in the Office - display_categories: THERMOSTAT \ No newline at end of file diff --git a/assistants/alexa/areas/spare_room/climate.spare_room.yaml b/assistants/alexa/areas/spare_room/climate.spare_room.yaml deleted file mode 100644 index 0d21e31..0000000 --- a/assistants/alexa/areas/spare_room/climate.spare_room.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Entity configuration for Amazon Alexa. -# https://www.nabucasa.com/config/amazon_alexa/ - -climate.spare_room: - name: Spare Room Radiator - description: Radiator in the Spare Room - display_categories: THERMOSTAT \ No newline at end of file diff --git a/automations/areas/master_bedroom/jon_bedside_remote/bedside_lamp_toggle.yaml b/automations/areas/master_bedroom/jon_bedside_remote/bedside_lamp_toggle.yaml deleted file mode 100644 index ae012ad..0000000 --- a/automations/areas/master_bedroom/jon_bedside_remote/bedside_lamp_toggle.yaml +++ /dev/null @@ -1,16 +0,0 @@ -alias: Jon Bedside Remote - Lamp Toggle -description: Toggles bedside lamp on and off -trigger: -- event_data: - event: 1002 - unique_id: "00:0d:6f:ff:fe:4d:61:69" - event_type: deconz_event - platform: event -condition: [] -action: -- service: light.toggle - data: - entity_id: light.master_bedroom_jon_bedside_lamp - transition: 0.5 - brightness: 255 - \ No newline at end of file diff --git a/automations/areas/master_bedroom/lee_bedside_remote/bedside_lamp_toggle.yaml b/automations/areas/master_bedroom/lee_bedside_remote/bedside_lamp_toggle.yaml deleted file mode 100644 index 270cd93..0000000 --- a/automations/areas/master_bedroom/lee_bedside_remote/bedside_lamp_toggle.yaml +++ /dev/null @@ -1,16 +0,0 @@ -alias: Lee Bedside Remote - Lamp Toggle -description: Toggles bedside lamp on and off -trigger: -- event_data: - event: 1002 - unique_id: "ec:1b:bd:ff:fe:1a:81:d8" - event_type: deconz_event - platform: event -condition: [] -action: -- service: light.toggle - data: - entity_id: light.master_bedroom_lee_bedside_lamp - transition: 0.5 - brightness: 255 - \ No newline at end of file diff --git a/automations/system/auto_snapshot.yaml b/automations/system/auto_snapshot.yaml deleted file mode 100644 index 6c50dc8..0000000 --- a/automations/system/auto_snapshot.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -# Automatic Home Assistant Snapshot -# Takes a full snapshot of home assistant every sunday at 0300hrs - -alias: HASSIO Weekly Snapshot -trigger: - - platform: time - at: "3:00:00" -condition: - condition: time - weekday: - - sun -action: - - service: hassio.snapshot_full - data_template: - name: "Automated Backup {{ now().strftime('%Y-%m-%d') }}" diff --git a/automations/system/recorder_purge.yaml b/automations/system/recorder_purge.yaml new file mode 100644 index 0000000..6ac017b --- /dev/null +++ b/automations/system/recorder_purge.yaml @@ -0,0 +1,12 @@ +--- +# Purge my recorder database every hour. + +description: Purge Recorder Database +alias: system_recorder_purge +id: 7cce89e8-662c-4e2d-9e56-5b76cc8adb66 +mode: single +trigger: + - platform: time_pattern + minutes: 12 +action: + - service: recorder.purge \ No newline at end of file diff --git a/automations/system/recorder_repack.yaml b/automations/system/recorder_repack.yaml new file mode 100644 index 0000000..72af837 --- /dev/null +++ b/automations/system/recorder_repack.yaml @@ -0,0 +1,20 @@ +--- +# Sunday morning, early morning, repack the recorder database. +# This keeps the disk usage low and nice by reclaiming space +# taken by the MySQL InnoDB database. + +description: Repack Recorder Database +alias: system_recorder_repack +id: 752639e0-9527-45a6-84e8-f54537d40278 +mode: single +trigger: + - platform: time + at: '05:55' +condition: + - condition: time + weekday: + - sun +action: + - service: recorder.purge + data: + repack: true \ No newline at end of file diff --git a/automations/system/travis_update_ha_config.yaml b/automations/system/travis_update_ha_config.yaml deleted file mode 100644 index 3bdcdea..0000000 --- a/automations/system/travis_update_ha_config.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -# Automatic pull of updated config from github if Travis CI build is successful -# https://www.home-assistant.io/components/travisci/ - -alias: Update HA Config On Successful TravisCI Build -trigger: - - platform: state - entity_id: sensor.jonathanadams_home_assistant_config_last_build_id -condition: - - condition: state - entity_id: sensor.jonathanadams_home_assistant_config_last_build_state - state: "passed" -action: - - service: hassio.addon_start - data: - addon: "core_git_pull" diff --git a/configuration.yaml b/configuration.yaml index 23b9fdc..a8f4b7f 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -1,3 +1,5 @@ +--- +# Main Home Assistant Configuration File + homeassistant: - # Load packages - packages: !include_dir_named integrations \ No newline at end of file + packages: !include_dir_named integrations diff --git a/custom_components/alexa_media/.translations/de.json b/custom_components/alexa_media/.translations/de.json deleted file mode 100644 index e1907ec..0000000 --- a/custom_components/alexa_media/.translations/de.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "config": { - "error": { - "identifier_exists": "Diese Email ist bereits registriert", - "invalid_credentials": "Falsche Zugangsdaten" - }, - "step": { - "user": { - "data": { - "password": "Passwort", - "email": "Email Adresse", - "url": "Amazon Region (z.B., amazon.de)", - "include_devices": "Eingebundene Geräte (komma getrennnt)", - "exclude_devices": "Ausgeschlossene Geräte (komma getrennnt)", - "debug": "Erweitertes debugging", - "scan_interval": "Sekunden zwischen den Scans" - }, - "description": "Bitte geben Sie ihre Informationen ein.", - "title": "Alexa Media Player - Konfiguration" - }, - "captcha": { - "data": { - "password": "Passwort", - "captcha": "Captcha" - }, - "description": "**{email} - alexa.{url}**\n{message}\n {captcha_image}", - "title": "Alexa Media Player - Captcha" - }, - "twofactor": { - "data": { - "securitycode": "2FA Code" - }, - "description": "**{email} - alexa.{url}**\nDen Zwei Faktor Pin eingeben.\n{message}", - "title": "Alexa Media Player - Zwei Faktor Authentifizierung" - }, - "authselect": { - "data": { - "authselectoption": "Einmal Pin Methode" - }, - "description": "**{email} - alexa.{url}**\n{message}", - "title": "Alexa Media Player - Einmal Pin Passwort" - }, - "claimspicker": { - "data": { - "authselectoption": "Verifizierungs Methode" - }, - "description": "**{email} - alexa.{url}**\nBitte Verifizierungs Methode auswählen. (z.B., `sms` oder `email`)\n{message}", - "title": "Alexa Media Player - Verifizierungs Methode" - } - - }, - "title": "Alexa Media Player" - } -} diff --git a/custom_components/alexa_media/.translations/en.json b/custom_components/alexa_media/.translations/en.json deleted file mode 100644 index c6d4dcf..0000000 --- a/custom_components/alexa_media/.translations/en.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "config": { - "error": { - "connection_error": "Error connecting; check network and retry", - "identifier_exists": "Email for Alexa URL already registered", - "invalid_credentials": "Invalid credentials", - "unknown_error": "Unknown error, please report with logs" - }, - "step": { - "user": { - "data": { - "password": "Password", - "email": "Email Address", - "url": "Amazon region domain (e.g., amazon.co.uk)", - "include_devices": "Included device (comma separated)", - "exclude_devices": "Excluded device (comma separated)", - "debug": "Advanced debugging", - "scan_interval": "Seconds between scans" - }, - "description": "Please enter your information.", - "title": "Alexa Media Player - Configuration" - }, - "captcha": { - "data": { - "password": "Password", - "captcha": "Captcha" - }, - "description": "**{email} - alexa.{url}**\n{message}\n {captcha_image}", - "title": "Alexa Media Player - Captcha" - }, - "twofactor": { - "data": { - "securitycode": "2FA Code" - }, - "description": "**{email} - alexa.{url}**\nEnter the One Time Password (OTP).\n{message}", - "title": "Alexa Media Player - Two Factor Authentication" - }, - "authselect": { - "data": { - "authselectoption": "OTP method" - }, - "description": "**{email} - alexa.{url}**\n{message}", - "title": "Alexa Media Player - One Time Password" - }, - "claimspicker": { - "data": { - "authselectoption": "Verification method" - }, - "description": "**{email} - alexa.{url}**\nPlease select verification method. (e.g., `sms` or `email`)\n{message}", - "title": "Alexa Media Player - Verification Method" - } - - }, - "title": "Alexa Media Player" - } -} diff --git a/custom_components/alexa_media/.translations/it.json b/custom_components/alexa_media/.translations/it.json deleted file mode 100644 index 29d7af4..0000000 --- a/custom_components/alexa_media/.translations/it.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "config": { - "error": { - "identifier_exists": "L'Email per l'URL di Alexa è già stata registrata", - "invalid_credentials": "Credenziali non valide" - }, - "step": { - "user": { - "data": { - "password": "Password", - "email": "Indirizzo email", - "url": "Regione del dominio Amazon (e.g., amazon.it)", - "include_devices": "Dispositivi da includere (separati da virgola)", - "exclude_devices": "Dispositivi da escludere (separati da virgola)", - "debug": "Debug avanzato", - "scan_interval": "Tempo in secondi fra le scansioni" - }, - "description": "Prego inserisci le tue informazioni.", - "title": "Alexa Media Player - Configurazione" - }, - "captcha": { - "data": { - "password": "Password", - "captcha": "Captcha" - }, - "description": "**{email} - alexa.{url}**\n{message}\n {captcha_image}", - "title": "Alexa Media Player - Captcha" - }, - "twofactor": { - "data": { - "securitycode": "Codice autenticazione a 2 fattori (2FA)" - }, - "description": "**{email} - alexa.{url}**\nInserisci la password usa e getta (OTP).\n{message}", - "title": "Alexa Media Player - Autenticazione a Due Fattori" - }, - "authselect": { - "data": { - "authselectoption": "Metodo password usa e getta (OTP)" - }, - "description": "**{email} - alexa.{url}**\n{message}", - "title": "Alexa Media Player - Password Usa e Getta (One Time Password)" - }, - "claimspicker": { - "data": { - "authselectoption": "Metodi di Verifica" - }, - "description": "**{email} - alexa.{url}**\nPrego selezionare un metodo di verifica. (e.g., `sms` or `email`)\n{message}", - "title": "Alexa Media Player - Metodi di Verifica" - } - - }, - "title": "Alexa Media Player" - } -} diff --git a/custom_components/alexa_media/.translations/nl.json b/custom_components/alexa_media/.translations/nl.json deleted file mode 100644 index 21bdf62..0000000 --- a/custom_components/alexa_media/.translations/nl.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "config": { - "error": { - "identifier_exists": "Dit e-mailadres is reeds geregistreerd", - "invalid_credentials": "Ongeldige inloggegevens" - }, - "step": { - "user": { - "data": { - "password": "Paswoord", - "email": "E-mailadres", - "url": "Domeinnaam van Amazon regio (bv.amazon.co.uk)", - "include_devices": "Apparaten toevoegen (Scheiding: komma)", - "exclude_devices": "Apparaten uitsluiten (Scheiding: komma)", - "debug": "Geavanceerd debuggen", - "scan_interval": "Aantal seconden tussen scans" - }, - "description": "Vul je gegevens in a.u.b.", - "title": "Alexa Media Player - Configuratie" - }, - "captcha": { - "data": { - "password": "Paswoord", - "captcha": "Captcha" - }, - "description": "**{email} - alexa.{url}**\n{message}\n {captcha_image}", - "title": "Alexa Media Player - Captcha" - }, - "twofactor": { - "data": { - "securitycode": "Verificatiecode" - }, - "description": "**{email} - alexa.{url}**\nGeef de verificatiecode in.\n{message}", - "title": "Alexa Media Player - Tweestapsverificatie" - }, - "authselect": { - "data": { - "authselectoption": "Eenmalige Pincode" - }, - "description": "**{email} - alexa.{url}**\n{message}", - "title": "Alexa Media Player - Eenmalige Pincode" - }, - "claimspicker": { - "data": { - "authselectoption": "Verificatiemethode" - }, - "description": "**{email} - alexa.{url}**\nSelecteer de verificatiemethode (bv.`sms` of `email`)\n{message}", - "title": "Alexa Media Player - Verificatiemethode" - } - - }, - "title": "Alexa Media Player" - } -} diff --git a/custom_components/alexa_media/.translations/pl.json b/custom_components/alexa_media/.translations/pl.json deleted file mode 100644 index 58b995d..0000000 --- a/custom_components/alexa_media/.translations/pl.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "config": { - "error": { - "identifier_exists": "Adres email dla Alexy już jest zarejestrowany", - "invalid_credentials": "Nieprawidłowe dane logowania" - }, - "step": { - "user": { - "data": { - "password": "Hasło", - "email": "Adres email", - "url": "Region/domena Amazon (np. amazon.co.uk)", - "include_devices": "Dodawane urządzenia (oddzielone przecinkami)", - "exclude_devices": "Wykluczone urządzenia (oddzielone przecinkami)", - "debug": "Zaawansowane debugowanie", - "scan_interval": "Interwał skanowania (sekundy)" - }, - "description": "Wprowadź dane", - "title": "Alexa Media Player - konfiguracja" - }, - "captcha": { - "data": { - "password": "Hasło", - "captcha": "Kod Captcha" - }, - "description": "**{email} - alexa.{url}**\n{message}\n {captcha_image}", - "title": "Alexa Media Player - kod Captcha" - }, - "twofactor": { - "data": { - "securitycode": "Kod uwierzytelniania dwuskładnikowego" - }, - "description": "**{email} - alexa.{url}**\nWprowadź hasło jednorazowe (OTP).\n{message}", - "title": "Alexa Media Player - uwierzytelnianie dwuskładnikowe" - }, - "authselect": { - "data": { - "authselectoption": "Metoda OTP" - }, - "description": "**{email} - alexa.{url}**\n{message}", - "title": "Alexa Media Player - hasło jednorazowe" - }, - "claimspicker": { - "data": { - "authselectoption": "Metoda weryfikacji" - }, - "description": "**{email} - alexa.{url}**\nWybierz metodę weryfikacji. (np., `sms` lub `email`)\n{message}", - "title": "Alexa Media Player - metoda weryfikacji" - } - - }, - "title": "Alexa Media Player" - } -} diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py deleted file mode 100644 index 196c16b..0000000 --- a/custom_components/alexa_media/__init__.py +++ /dev/null @@ -1,1064 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Support to interface with Alexa Devices. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -from datetime import datetime, timedelta -import logging -from typing import List, Optional, Text - -from alexapy import AlexapyLoginError, WebsocketEchoClient, hide_email, hide_serial -from homeassistant import util -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_EMAIL, - CONF_NAME, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_URL, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.event import async_call_later -from homeassistant.util import dt -import voluptuous as vol - -from .config_flow import configured_instances -from .const import ( - ALEXA_COMPONENTS, - ATTR_EMAIL, - ATTR_NUM_ENTRIES, - CONF_ACCOUNTS, - CONF_DEBUG, - CONF_EXCLUDE_DEVICES, - CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA, - DOMAIN, - ISSUE_URL, - MIN_TIME_BETWEEN_FORCED_SCANS, - MIN_TIME_BETWEEN_SCANS, - SCAN_INTERVAL, - SERVICE_CLEAR_HISTORY, - SERVICE_UPDATE_LAST_CALLED, - STARTUP, -) -from .helpers import retry_async - -_LOGGER = logging.getLogger(__name__) - - -ACCOUNT_CONFIG_SCHEMA = vol.Schema( - { - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_DEBUG, default=False): cv.boolean, - vol.Optional(CONF_INCLUDE_DEVICES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXCLUDE_DEVICES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_ACCOUNTS): vol.All( - cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA] - ) - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -CLEAR_HISTORY_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_EMAIL, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_NUM_ENTRIES, default=50): vol.All( - int, vol.Range(min=1, max=50) - ), - } -) - -LAST_CALL_UPDATE_SCHEMA = vol.Schema( - {vol.Optional(ATTR_EMAIL, default=[]): vol.All(cv.ensure_list, [cv.string])} -) - - -async def async_setup(hass, config, discovery_info=None): - # pylint: disable=unused-argument - """Set up the Alexa domain.""" - if DOMAIN not in config: - return True - - domainconfig = config.get(DOMAIN) - for account in domainconfig[CONF_ACCOUNTS]: - entry_title = "{} - {}".format(account[CONF_EMAIL], account[CONF_URL]) - if entry_title in configured_instances(hass): - for entry in hass.config_entries.async_entries(DOMAIN): - if entry_title == entry.title: - hass.config_entries.async_update_entry( - entry, - data={ - CONF_EMAIL: account[CONF_EMAIL], - CONF_PASSWORD: account[CONF_PASSWORD], - CONF_URL: account[CONF_URL], - CONF_DEBUG: account[CONF_DEBUG], - CONF_INCLUDE_DEVICES: account[CONF_INCLUDE_DEVICES], - CONF_EXCLUDE_DEVICES: account[CONF_EXCLUDE_DEVICES], - CONF_SCAN_INTERVAL: account[ - CONF_SCAN_INTERVAL - ].total_seconds(), - }, - ) - break - else: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_EMAIL: account[CONF_EMAIL], - CONF_PASSWORD: account[CONF_PASSWORD], - CONF_URL: account[CONF_URL], - CONF_DEBUG: account[CONF_DEBUG], - CONF_INCLUDE_DEVICES: account[CONF_INCLUDE_DEVICES], - CONF_EXCLUDE_DEVICES: account[CONF_EXCLUDE_DEVICES], - CONF_SCAN_INTERVAL: account[CONF_SCAN_INTERVAL].total_seconds(), - }, - ) - ) - return True - - -@retry_async(limit=5, delay=5, catch_exceptions=True) -async def async_setup_entry(hass, config_entry): - """Set up Alexa Media Player as config entry.""" - - async def close_alexa_media(event=None) -> None: - """Clean up Alexa connections.""" - _LOGGER.debug("Received shutdown request: %s", event) - for email, _ in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - await close_connections(hass, email) - - hass.data.setdefault(DATA_ALEXAMEDIA, {"accounts": {}}) - from alexapy import AlexaLogin, __version__ as alexapy_version - - _LOGGER.info(STARTUP) - _LOGGER.info("Loaded alexapy==%s", alexapy_version) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_alexa_media) - account = config_entry.data - email = account.get(CONF_EMAIL) - password = account.get(CONF_PASSWORD) - url = account.get(CONF_URL) - hass.data[DATA_ALEXAMEDIA]["accounts"].setdefault( - email, - { - "config_entry": config_entry, - "setup_platform_callback": setup_platform_callback, - "test_login_status": test_login_status, - "devices": {"media_player": {}, "switch": {}}, - "entities": {"media_player": {}, "switch": {}}, - "excluded": {}, - "new_devices": True, - "websocket_lastattempt": 0, - "websocketerror": 0, - "websocket_commands": {}, - "websocket_activity": {"serials": {}, "refreshed": {}}, - "websocket": None, - "auth_info": None, - "configurator": [], - }, - ) - login = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( - "login_obj", - AlexaLogin(url, email, password, hass.config.path, account.get(CONF_DEBUG)), - ) - await login.login_with_cookie() - await test_login_status(hass, config_entry, login, setup_platform_callback) - return True - - -async def setup_platform_callback(hass, config_entry, login, callback_data): - """Handle response from configurator. - - Args: - callback_data (json): Returned data from configurator passed through - request_configuration and configuration_callback - - """ - _LOGGER.debug( - ( - "Configurator closed for Status: %s\n" - " got captcha: %s securitycode: %s" - " Claimsoption: %s AuthSelectOption: %s " - " VerificationCode: %s" - ), - login.status, - callback_data.get("captcha"), - callback_data.get("securitycode"), - callback_data.get("claimsoption"), - callback_data.get("authselectoption"), - callback_data.get("verificationcode"), - ) - await login.login(data=callback_data) - await test_login_status(hass, config_entry, login, setup_platform_callback) - - -async def request_configuration(hass, config_entry, login, setup_platform_callback): - """Request configuration steps from the user using the configurator.""" - - async def configuration_callback(callback_data): - """Handle the submitted configuration.""" - await hass.async_add_job( - setup_platform_callback, hass, config_entry, login, callback_data - ) - - configurator = hass.components.configurator - status = login.status - email = login.email - # links = "" - footer = "" - if "error_message" in status and status["error_message"]: - footer = ( - "\nNOTE: Actual Amazon error message in red below. " - "Remember password will be provided automatically" - " and Amazon error message normally appears first!" - ) - # if login.links: - # links = '\n\nGo to link with link# (e.g. link0)\n' + login.links - # Get Captcha - if ( - status - and "captcha_image_url" in status - and status["captcha_image_url"] is not None - ): - config_id = configurator.async_request_config( - "Alexa Media Player - Captcha - {}".format(email), - configuration_callback, - description=( - "Please enter the text for the captcha." - " Please hit confirm to reload image." - # + links - + footer - ), - description_image=status["captcha_image_url"], - submit_caption="Confirm", - fields=[{"id": "captcha", "name": "Captcha"}], - ) - elif ( - status and "securitycode_required" in status and status["securitycode_required"] - ): # Get 2FA code - config_id = configurator.async_request_config( - "Alexa Media Player - 2FA - {}".format(email), - configuration_callback, - description=( - "Please enter your Two-Factor Security code." - # + links - + footer - ), - submit_caption="Confirm", - fields=[{"id": "securitycode", "name": "Security Code"}], - ) - elif ( - status and "claimspicker_required" in status and status["claimspicker_required"] - ): # Get picker method - options = status["claimspicker_message"] - if options: - config_id = configurator.async_request_config( - "Alexa Media Player - Verification Method - {}".format(email), - configuration_callback, - description=( - "Please select the verification method. " - "(e.g., `sms` or `email`).\n{}".format(options) - # + links - + footer - ), - submit_caption="Confirm", - fields=[{"id": "claimsoption", "name": "Option"}], - ) - else: - await configuration_callback({}) - elif ( - status and "authselect_required" in status and status["authselect_required"] - ): # Get picker method - options = status["authselect_message"] - if options: - config_id = configurator.async_request_config( - "Alexa Media Player - OTP Method - {}".format(email), - configuration_callback, - description=( - "Please select the OTP method. " - "(e.g., `0`, `1`).
{}".format(options) - # + links - + footer - ), - submit_caption="Confirm", - fields=[{"id": "authselectoption", "name": "Option"}], - ) - else: - await configuration_callback({}) - elif ( - status - and "verificationcode_required" in status - and status["verificationcode_required"] - ): # Get picker method - config_id = configurator.async_request_config( - "Alexa Media Player - Verification Code - {}".format(email), - configuration_callback, - description=( - "Please enter received verification code." - # + links - + footer - ), - submit_caption="Confirm", - fields=[{"id": "verificationcode", "name": "Verification Code"}], - ) - else: # Check login - config_id = configurator.async_request_config( - "Alexa Media Player - Begin - {}".format(email), - configuration_callback, - description=("Please hit confirm to begin login attempt."), - submit_caption="Confirm", - fields=[], - ) - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"].append(config_id) - if "error_message" in status and status["error_message"]: - configurator.async_notify_errors(config_id, status["error_message"]) - if len(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"]) > 1: - configurator.async_request_done( - (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"]).pop(0) - ) - - -async def test_login_status(hass, config_entry, login, setup_platform_callback) -> None: - """Test the login status and spawn requests for info.""" - _LOGGER.debug("Testing login status: %s", login.status) - if "login_successful" in login.status and login.status["login_successful"]: - _LOGGER.debug("Setting up Alexa devices for %s", hide_email(login.email)) - await hass.async_add_job(setup_alexa, hass, config_entry, login) - return - if "captcha_required" in login.status and login.status["captcha_required"]: - _LOGGER.debug("Creating configurator to request captcha") - elif ( - "securitycode_required" in login.status - and login.status["securitycode_required"] - ): - _LOGGER.debug("Creating configurator to request 2FA") - elif ( - "claimspicker_required" in login.status - and login.status["claimspicker_required"] - ): - _LOGGER.debug("Creating configurator to select verification option") - elif "authselect_required" in login.status and login.status["authselect_required"]: - _LOGGER.debug("Creating configurator to select OTA option") - elif ( - "verificationcode_required" in login.status - and login.status["verificationcode_required"] - ): - _LOGGER.debug("Creating configurator to enter verification code") - elif "login_failed" in login.status and login.status["login_failed"]: - _LOGGER.debug("Creating configurator to start new login attempt") - await hass.async_add_job( - request_configuration, hass, config_entry, login, setup_platform_callback - ) - - -async def setup_alexa(hass, config_entry, login_obj): - """Set up a alexa api based on host parameter.""" - - def _existing_serials() -> List: - email: Text = login_obj.email - existing_serials = ( - list( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][ - "media_player" - ].keys() - ) - if "entities" in (hass.data[DATA_ALEXAMEDIA]["accounts"][email]) - else [] - ) - for serial in existing_serials: - device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ - "media_player" - ][serial] - if "appDeviceList" in device and device["appDeviceList"]: - apps = list( - map( - lambda x: x["serialNumber"] if "serialNumber" in x else None, - device["appDeviceList"], - ) - ) - # _LOGGER.debug("Combining %s with %s", - # existing_serials, apps) - existing_serials = existing_serials + apps - return existing_serials - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - async def update_devices(login_obj): - """Ping Alexa API to identify all devices, bluetooth, and last called device. - - This will add new devices and services when discovered. By default this - runs every SCAN_INTERVAL seconds unless another method calls it. if - websockets is connected, it will increase the delay 10-fold between updates. - While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to - reduce the number of runs to avoid flooding. Slow changing states - should be checked here instead of in spawned components like - media_player since this object is one per account. - Each AlexaAPI call generally results in two webpage requests. - """ - from alexapy import AlexaAPI - - email: Text = login_obj.email - if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]: - return - existing_serials = _existing_serials() - existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][ - "media_player" - ].values() - websocket_enabled = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( - "websocket" - ) - auth_info = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get("auth_info") - new_devices = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] - devices = {} - bluetooth = {} - preferences = {} - dnd = {} - raw_notifications = {} - try: - if new_devices: - auth_info = await AlexaAPI.get_authentication(login_obj) - devices = await AlexaAPI.get_devices(login_obj) - bluetooth = await AlexaAPI.get_bluetooth(login_obj) - preferences = await AlexaAPI.get_device_preferences(login_obj) - dnd = await AlexaAPI.get_dnd_state(login_obj) - raw_notifications = await AlexaAPI.get_notifications(login_obj) - _LOGGER.debug( - "%s: Found %s devices, %s bluetooth", - hide_email(email), - len(devices) if devices is not None else "", - len(bluetooth.get("bluetoothStates", [])) - if bluetooth is not None - else "", - ) - if (devices is None or bluetooth is None) and not ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"] - ): - raise AlexapyLoginError() - except (AlexapyLoginError, RuntimeError): - _LOGGER.debug( - "%s: Alexa API disconnected; attempting to relogin", hide_email(email) - ) - await login_obj.login_with_cookie() - await test_login_status( - hass, config_entry, login_obj, setup_platform_callback - ) - return - await process_notifications(login_obj, raw_notifications) - # Process last_called data to fire events - await update_last_called(login_obj) - - new_alexa_clients = [] # list of newly discovered device names - exclude_filter = [] - include_filter = [] - - for device in devices: - serial = device["serialNumber"] - dev_name = device["accountName"] - if include and dev_name not in include: - include_filter.append(dev_name) - if "appDeviceList" in device: - for app in device["appDeviceList"]: - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ - app["serialNumber"] - ] - ) = device - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ - serial - ] = device - continue - elif exclude and dev_name in exclude: - exclude_filter.append(dev_name) - if "appDeviceList" in device: - for app in device["appDeviceList"]: - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ - app["serialNumber"] - ] - ) = device - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ - serial - ] = device - continue - - if "bluetoothStates" in bluetooth: - for b_state in bluetooth["bluetoothStates"]: - if serial == b_state["deviceSerialNumber"]: - device["bluetooth_state"] = b_state - break - - if "devicePreferences" in preferences: - for dev in preferences["devicePreferences"]: - if dev["deviceSerialNumber"] == serial: - device["locale"] = dev["locale"] - device["timeZoneId"] = dev["timeZoneId"] - _LOGGER.debug( - "%s: Locale %s timezone %s", - dev_name, - device["locale"], - device["timeZoneId"], - ) - break - - if "doNotDisturbDeviceStatusList" in dnd: - for dev in dnd["doNotDisturbDeviceStatusList"]: - if dev["deviceSerialNumber"] == serial: - device["dnd"] = dev["enabled"] - _LOGGER.debug("%s: DND %s", dev_name, device["dnd"]) - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ - "switch" - ].setdefault(serial, {"dnd": True}) - - break - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["auth_info"] = device[ - "auth_info" - ] = auth_info - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"]["media_player"][ - serial - ] = device - - if serial not in existing_serials: - new_alexa_clients.append(dev_name) - elif serial in existing_entities: - await hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][ - "media_player" - ].get(serial).refresh(device, no_api=True) - _LOGGER.debug( - "%s: Existing: %s New: %s;" - " Filtered out by not being in include: %s " - "or in exclude: %s", - hide_email(email), - list(existing_entities), - new_alexa_clients, - include_filter, - exclude_filter, - ) - - if new_alexa_clients: - cleaned_config = config.copy() - cleaned_config.pop(CONF_PASSWORD, None) - # CONF_PASSWORD contains sensitive info which is no longer needed - for component in ALEXA_COMPONENTS: - if component == "notify": - hass.async_create_task( - async_load_platform( - hass, - component, - DOMAIN, - {CONF_NAME: DOMAIN, "config": cleaned_config}, - config, - ) - ) - else: - hass.async_add_job( - hass.config_entries.async_forward_entry_setup( - config_entry, component - ) - ) - - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False - async_call_later( - hass, - scan_interval if not websocket_enabled else scan_interval * 10, - lambda _: hass.async_create_task( - update_devices( # pylint: disable=unexpected-keyword-arg - login_obj, no_throttle=True - ) - ), - ) - - async def process_notifications(login_obj, raw_notifications=None): - """Process raw notifications json.""" - from alexapy import AlexaAPI - - if not raw_notifications: - raw_notifications = await AlexaAPI.get_notifications(login_obj) - email: Text = login_obj.email - notifications = {"process_timestamp": datetime.utcnow()} - for notification in raw_notifications: - n_dev_id = notification["deviceSerialNumber"] - n_type = notification["type"] - if n_type == "MusicAlarm": - n_type = "Alarm" - n_id = notification["notificationIndex"] - n_date = notification["originalDate"] - n_time = notification["originalTime"] - notification["date_time"] = f"{n_date} {n_time}" - if n_dev_id not in notifications: - notifications[n_dev_id] = {} - if n_type not in notifications[n_dev_id]: - notifications[n_dev_id][n_type] = {} - notifications[n_dev_id][n_type][n_id] = notification - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["notifications"] = notifications - _LOGGER.debug( - "%s: Updated %s notifications for %s devices at %s", - hide_email(email), - len(raw_notifications), - len(notifications), - dt.as_local( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["notifications"][ - "process_timestamp" - ] - ), - ) - - async def update_last_called(login_obj, last_called=None): - """Update the last called device for the login_obj. - - This will store the last_called in hass.data and also fire an event - to notify listeners. - """ - from alexapy import AlexaAPI - - if not last_called: - last_called = await AlexaAPI.get_last_device_serial(login_obj) - _LOGGER.debug( - "%s: Updated last_called: %s", hide_email(email), hide_serial(last_called) - ) - stored_data = hass.data[DATA_ALEXAMEDIA]["accounts"][email] - if ( - "last_called" in stored_data and last_called != stored_data["last_called"] - ) or ("last_called" not in stored_data and last_called is not None): - _LOGGER.debug( - "%s: last_called changed: %s to %s", - hide_email(email), - hide_serial( - stored_data["last_called"] if "last_called" in stored_data else None - ), - hide_serial(last_called), - ) - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"last_called_change": last_called}, - ) - (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["last_called"]) = last_called - - async def update_bluetooth_state(login_obj, device_serial): - """Update the bluetooth state on ws bluetooth event.""" - from alexapy import AlexaAPI - - bluetooth = await AlexaAPI.get_bluetooth(login_obj) - device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ - "media_player" - ][device_serial] - - if "bluetoothStates" in bluetooth: - for b_state in bluetooth["bluetoothStates"]: - if device_serial == b_state["deviceSerialNumber"]: - # _LOGGER.debug("%s: setting value for: %s to %s", - # hide_email(email), - # hide_serial(device_serial), - # hide_serial(b_state)) - device["bluetooth_state"] = b_state - return device["bluetooth_state"] - _LOGGER.debug( - "%s: get_bluetooth for: %s failed with %s", - hide_email(email), - hide_serial(device_serial), - hide_serial(bluetooth), - ) - return None - - async def clear_history(call): - """Handle clear history service request. - - Arguments - call.ATTR_EMAIL {List[str: None]} -- Case-sensitive Alexa emails. - Default is all known emails. - call.ATTR_NUM_ENTRIES {int: 50} -- Number of entries to delete. - - Returns - bool -- True if deletion successful - - """ - from alexapy import AlexaAPI - - requested_emails = call.data.get(ATTR_EMAIL) - items: int = int(call.data.get(ATTR_NUM_ENTRIES)) - - _LOGGER.debug( - "Service clear_history called for: %i items for %s", items, requested_emails - ) - for email, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - if requested_emails and email not in requested_emails: - continue - login_obj = account_dict["login_obj"] - return await AlexaAPI.clear_history(login_obj, items) - - async def last_call_handler(call): - """Handle last call service request. - - Args: - call.ATTR_EMAIL: List of case-sensitive Alexa email addresses. If None - all accounts are updated. - - """ - requested_emails = call.data.get(ATTR_EMAIL) - _LOGGER.debug("Service update_last_called for: %s", requested_emails) - for email, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - if requested_emails and email not in requested_emails: - continue - login_obj = account_dict["login_obj"] - await update_last_called(login_obj) - - async def ws_connect() -> WebsocketEchoClient: - """Open WebSocket connection. - - This will only attempt one login before failing. - """ - websocket: Optional[WebsocketEchoClient] = None - try: - websocket = WebsocketEchoClient( - login_obj, - ws_handler, - ws_open_handler, - ws_close_handler, - ws_error_handler, - ) - _LOGGER.debug("%s: Websocket created: %s", hide_email(email), websocket) - await websocket.async_run() - except BaseException as exception_: # pylint: disable=broad-except - _LOGGER.debug( - "%s: Websocket creation failed: %s", hide_email(email), exception_ - ) - return - return websocket - - async def ws_handler(message_obj): - """Handle websocket messages. - - This allows push notifications from Alexa to update last_called - and media state. - """ - import time - - command = ( - message_obj.json_payload["command"] - if isinstance(message_obj.json_payload, dict) - and "command" in message_obj.json_payload - else None - ) - json_payload = ( - message_obj.json_payload["payload"] - if isinstance(message_obj.json_payload, dict) - and "payload" in message_obj.json_payload - else None - ) - existing_serials = _existing_serials() - seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ - "websocket_commands" - ] - if command and json_payload: - - _LOGGER.debug( - "%s: Received websocket command: %s : %s", - hide_email(email), - command, - hide_serial(json_payload), - ) - serial = None - if command not in seen_commands: - seen_commands[command] = time.time() - _LOGGER.debug("Adding %s to seen_commands: %s", command, seen_commands) - if ( - "dopplerId" in json_payload - and "deviceSerialNumber" in json_payload["dopplerId"] - ): - serial = json_payload["dopplerId"]["deviceSerialNumber"] - elif ( - "key" in json_payload - and "entryId" in json_payload["key"] - and json_payload["key"]["entryId"].find("#") != -1 - ): - serial = (json_payload["key"]["entryId"]).split("#")[2] - else: - serial = None - if command == "PUSH_ACTIVITY": - # Last_Alexa Updated - last_called = { - "serialNumber": serial, - "timestamp": json_payload["timestamp"], - } - if serial and serial in existing_serials: - await update_last_called(login_obj, last_called) - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"push_activity": json_payload}, - ) - elif command in ( - "PUSH_AUDIO_PLAYER_STATE", - "PUSH_MEDIA_CHANGE", - "PUSH_MEDIA_PROGRESS_CHANGE", - ): - # Player update/ Push_media from tune_in - if serial and serial in existing_serials: - _LOGGER.debug( - "Updating media_player: %s", hide_serial(json_payload) - ) - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"player_state": json_payload}, - ) - elif command == "PUSH_VOLUME_CHANGE": - # Player volume update - if serial and serial in existing_serials: - _LOGGER.debug( - "Updating media_player volume: %s", hide_serial(json_payload) - ) - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"player_state": json_payload}, - ) - elif command in ( - "PUSH_DOPPLER_CONNECTION_CHANGE", - "PUSH_EQUALIZER_STATE_CHANGE", - ): - # Player availability update - if serial and serial in existing_serials: - _LOGGER.debug( - "Updating media_player availability %s", - hide_serial(json_payload), - ) - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"player_state": json_payload}, - ) - elif command == "PUSH_BLUETOOTH_STATE_CHANGE": - # Player bluetooth update - bt_event = json_payload["bluetoothEvent"] - bt_success = json_payload["bluetoothEventSuccess"] - if ( - serial - and serial in existing_serials - and bt_success - and bt_event - and bt_event in ["DEVICE_CONNECTED", "DEVICE_DISCONNECTED"] - ): - _LOGGER.debug( - "Updating media_player bluetooth %s", hide_serial(json_payload) - ) - bluetooth_state = await update_bluetooth_state(login_obj, serial) - # _LOGGER.debug("bluetooth_state %s", - # hide_serial(bluetooth_state)) - if bluetooth_state: - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"bluetooth_change": bluetooth_state}, - ) - elif command == "PUSH_MEDIA_QUEUE_CHANGE": - # Player availability update - if serial and serial in existing_serials: - _LOGGER.debug( - "Updating media_player queue %s", hide_serial(json_payload) - ) - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"queue_state": json_payload}, - ) - elif command == "PUSH_NOTIFICATION_CHANGE": - # Player update - await process_notifications(login_obj) - if serial and serial in existing_serials: - _LOGGER.debug( - "Updating mediaplayer notifications: %s", - hide_serial(json_payload), - ) - hass.bus.async_fire( - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"notification_update": json_payload}, - ) - elif command in [ - "PUSH_DELETE_DOPPLER_ACTIVITIES", # delete Alexa history - "PUSH_LIST_ITEM_CHANGE", # update shopping list - "PUSH_CONTENT_FOCUS_CHANGE", # likely prime related refocus - ]: - pass - else: - _LOGGER.warning( - "Unhandled command: %s with data %s. Please report at %s", - command, - hide_serial(json_payload), - ISSUE_URL, - ) - if serial in existing_serials: - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_activity"][ - "serials" - ][serial] = time.time() - if ( - serial - and serial not in existing_serials - and serial - not in ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"].keys() - ) - ): - _LOGGER.debug("Discovered new media_player %s", serial) - (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"]) = True - await update_devices( # pylint: disable=unexpected-keyword-arg - login_obj, no_throttle=True - ) - - async def ws_open_handler(): - """Handle websocket open.""" - import time - - email: Text = login_obj.email - _LOGGER.debug("%s: Websocket succesfully connected", hide_email(email)) - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] - ) = 0 # set errors to 0 - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_lastattempt"] - ) = time.time() - - async def ws_close_handler(): - """Handle websocket close. - - This should attempt to reconnect up to 5 times - """ - from asyncio import sleep - import time - - email: Text = login_obj.email - errors: int = (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]) - delay: int = 5 * 2 ** errors - last_attempt = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ - "websocket_lastattempt" - ] - now = time.time() - if (now - last_attempt) < delay: - return - while errors < 5 and not ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] - ): - _LOGGER.debug( - "%s: Websocket closed; reconnect #%i in %is", - hide_email(email), - errors, - delay, - ) - await sleep(delay) - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_lastattempt"] - ) = time.time() - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] - ) = await ws_connect() - errors += 1 - delay = 5 * 2 ** errors - _LOGGER.debug( - "%s: Websocket closed; retries exceeded; polling", hide_email(email) - ) - (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]) = None - await update_devices( # pylint: disable=unexpected-keyword-arg - login_obj, no_throttle=True - ) - - async def ws_error_handler(message): - """Handle websocket error. - - This currently logs the error. In the future, this should invalidate - the websocket and determine if a reconnect should be done. By - specification, websockets will issue a close after every error. - """ - email = login_obj.email - errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] - _LOGGER.debug( - "%s: Received websocket error #%i %s", hide_email(email), errors, message - ) - (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]) = None - (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]) = errors + 1 - - config = config_entry.data - email = config.get(CONF_EMAIL) - include = config.get(CONF_INCLUDE_DEVICES) - exclude = config.get(CONF_EXCLUDE_DEVICES) - scan_interval: float = ( - config.get(CONF_SCAN_INTERVAL).total_seconds() - if isinstance(config.get(CONF_SCAN_INTERVAL), timedelta) - else config.get(CONF_SCAN_INTERVAL) - ) - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] = login_obj - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] = await ws_connect() - await update_devices( # pylint: disable=unexpected-keyword-arg - login_obj, no_throttle=True - ) - hass.services.async_register( - DOMAIN, - SERVICE_UPDATE_LAST_CALLED, - last_call_handler, - schema=LAST_CALL_UPDATE_SCHEMA, - ) - hass.services.async_register( - DOMAIN, SERVICE_CLEAR_HISTORY, clear_history, schema=CLEAR_HISTORY_SCHEMA - ) - # Clear configurator. We delay till here to avoid leaving a modal orphan - await clear_configurator(hass, email) - return True - - -async def async_unload_entry(hass, entry) -> bool: - """Unload a config entry.""" - hass.services.async_remove(DOMAIN, SERVICE_UPDATE_LAST_CALLED) - hass.services.async_remove(DOMAIN, SERVICE_CLEAR_HISTORY) - for component in ALEXA_COMPONENTS: - await hass.config_entries.async_forward_entry_unload(entry, component) - # notify has to be handled manually as the forward does not work yet - from .notify import async_unload_entry as notify_async_unload_entry - - await notify_async_unload_entry(hass, entry) - email = entry.data["email"] - await close_connections(hass, email) - await clear_configurator(hass, email) - hass.data[DATA_ALEXAMEDIA]["accounts"].pop(email) - _LOGGER.debug("Unloaded entry for %s", hide_email(email)) - return True - - -async def clear_configurator(hass, email: Text) -> None: - """Clear open configurators for email.""" - if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]: - return - if "configurator" in hass.data[DATA_ALEXAMEDIA]["accounts"][email]: - for config_id in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"]: - configurator = hass.components.configurator - configurator.async_request_done(config_id) - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"] = [] - - -async def close_connections(hass, email: Text) -> None: - """Clear open aiohttp connections for email.""" - if ( - email not in hass.data[DATA_ALEXAMEDIA]["accounts"] - or "login_obj" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email] - ): - return - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][email] - login_obj = account_dict["login_obj"] - await login_obj.close() - _LOGGER.debug( - "%s: Connection closed: %s", hide_email(email), login_obj._session.closed - ) - await clear_configurator(hass, email) diff --git a/custom_components/alexa_media/__pycache__/__init__.cpython-37.pyc b/custom_components/alexa_media/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 6532838..0000000 Binary files a/custom_components/alexa_media/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/alarm_control_panel.cpython-37.pyc b/custom_components/alexa_media/__pycache__/alarm_control_panel.cpython-37.pyc deleted file mode 100644 index 189b960..0000000 Binary files a/custom_components/alexa_media/__pycache__/alarm_control_panel.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/config_flow.cpython-37.pyc b/custom_components/alexa_media/__pycache__/config_flow.cpython-37.pyc deleted file mode 100644 index e69e2b2..0000000 Binary files a/custom_components/alexa_media/__pycache__/config_flow.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/const.cpython-37.pyc b/custom_components/alexa_media/__pycache__/const.cpython-37.pyc deleted file mode 100644 index 33577c8..0000000 Binary files a/custom_components/alexa_media/__pycache__/const.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/helpers.cpython-37.pyc b/custom_components/alexa_media/__pycache__/helpers.cpython-37.pyc deleted file mode 100644 index 6f94a99..0000000 Binary files a/custom_components/alexa_media/__pycache__/helpers.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/media_player.cpython-37.pyc b/custom_components/alexa_media/__pycache__/media_player.cpython-37.pyc deleted file mode 100644 index 9d29bfc..0000000 Binary files a/custom_components/alexa_media/__pycache__/media_player.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/notify.cpython-37.pyc b/custom_components/alexa_media/__pycache__/notify.cpython-37.pyc deleted file mode 100644 index 2d2bfc5..0000000 Binary files a/custom_components/alexa_media/__pycache__/notify.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/sensor.cpython-37.pyc b/custom_components/alexa_media/__pycache__/sensor.cpython-37.pyc deleted file mode 100644 index 6e66bd3..0000000 Binary files a/custom_components/alexa_media/__pycache__/sensor.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/__pycache__/switch.cpython-37.pyc b/custom_components/alexa_media/__pycache__/switch.cpython-37.pyc deleted file mode 100644 index 031e01e..0000000 Binary files a/custom_components/alexa_media/__pycache__/switch.cpython-37.pyc and /dev/null differ diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py deleted file mode 100644 index 4f4ddd4..0000000 --- a/custom_components/alexa_media/alarm_control_panel.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Alexa Devices Alarm Control Panel using Guard Mode. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -import logging -from typing import Dict, List, Text # noqa pylint: disable=unused-import - -from homeassistant import util -from homeassistant.components.alarm_control_panel import AlarmControlPanel -from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED -from homeassistant.helpers.event import async_call_later - -from . import ( - CONF_EMAIL, - CONF_EXCLUDE_DEVICES, - CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA, - DOMAIN as ALEXA_DOMAIN, - MIN_TIME_BETWEEN_FORCED_SCANS, - MIN_TIME_BETWEEN_SCANS, - hide_email, -) -from .helpers import _catch_login_errors, add_devices, retry_async - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = [ALEXA_DOMAIN] - - -@retry_async(limit=5, delay=2, catch_exceptions=True) -async def async_setup_platform( - hass, config, add_devices_callback, discovery_info=None -) -> bool: - """Set up the Alexa alarm control panel platform.""" - devices = [] # type: List[AlexaAlarmControlPanel] - account = config[CONF_EMAIL] - include_filter = config.get(CONF_INCLUDE_DEVICES, []) - exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - if "alarm_control_panel" not in (account_dict["entities"]): - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ - "alarm_control_panel" - ] - ) = {} - alexa_client: AlexaAlarmControlPanel = AlexaAlarmControlPanel( - account_dict["login_obj"] - ) - await alexa_client.init() - if not (alexa_client and alexa_client.unique_id): - _LOGGER.debug( - "%s: Skipping creation of uninitialized device: %s", - hide_email(account), - alexa_client, - ) - elif alexa_client.unique_id not in ( - account_dict["entities"]["alarm_control_panel"] - ): - devices.append(alexa_client) - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ - "alarm_control_panel" - ][alexa_client.unique_id] - ) = alexa_client - else: - _LOGGER.debug( - "%s: Skipping already added device: %s", hide_email(account), alexa_client - ) - return await add_devices( - hide_email(account), - devices, - add_devices_callback, - include_filter, - exclude_filter, - ) - - -async def async_setup_entry(hass, config_entry, async_add_devices): - """Set up the Alexa alarm control panel platform by config_entry.""" - return await async_setup_platform( - hass, config_entry.data, async_add_devices, discovery_info=None - ) - - -async def async_unload_entry(hass, entry) -> bool: - """Unload a config entry.""" - account = entry.data[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - for device in account_dict["entities"]["alarm_control_panel"].values(): - await device.async_remove() - return True - - -class AlexaAlarmControlPanel(AlarmControlPanel): - """Implementation of Alexa Media Player alarm control panel.""" - - def __init__(self, login) -> None: - # pylint: disable=unexpected-keyword-arg - """Initialize the Alexa device.""" - from alexapy import AlexaAPI - - # Class info - self._login = login - self.alexa_api = AlexaAPI(self, login) - self.alexa_api_session = login.session - self.account = hide_email(login.email) - - # Guard info - self._appliance_id = None - self._guard_entity_id = None - self._friendly_name = "Alexa Guard" - self._state = None - self._should_poll = False - self._attrs: Dict[Text, Text] = {} - - async def init(self): - """Initialize.""" - try: - from simplejson import JSONDecodeError - - data = await self.alexa_api.get_guard_details(self._login) - guard_dict = data["locationDetails"]["locationDetails"]["Default_Location"][ - "amazonBridgeDetails" - ]["amazonBridgeDetails"]["LambdaBridge_AAA/OnGuardSmartHomeBridgeService"][ - "applianceDetails" - ][ - "applianceDetails" - ] - except (KeyError, TypeError, JSONDecodeError): - guard_dict = {} - for _, value in guard_dict.items(): - if value["modelName"] == "REDROCK_GUARD_PANEL": - self._appliance_id = value["applianceId"] - self._guard_entity_id = value["entityId"] - self._friendly_name += " " + self._appliance_id[-5:] - _LOGGER.debug( - "%s: Discovered %s: %s %s", - self.account, - self._friendly_name, - self._appliance_id, - self._guard_entity_id, - ) - if not self._appliance_id: - _LOGGER.debug("%s: No Alexa Guard entity found", self.account) - - async def async_added_to_hass(self): - """Store register state change callback.""" - try: - if not self.enabled: - return - except AttributeError: - pass - # Register event handler on bus - self._listener = self.hass.bus.async_listen( - f"{ALEXA_DOMAIN}_{hide_email(self._login.email)}"[0:32], self._handle_event - ) - await self.async_update() - - async def async_will_remove_from_hass(self): - """Prepare to remove entity.""" - # Register event handler on bus - self._listener() - - def _handle_event(self, event): - """Handle websocket events. - - Used instead of polling. - """ - try: - if not self.enabled: - return - except AttributeError: - pass - if "push_activity" in event.data: - async_call_later( - self.hass, - 2, - lambda _: self.hass.async_create_task( - self.async_update(no_throttle=True) - ), - ) - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - @_catch_login_errors - async def async_update(self): - """Update Guard state.""" - try: - if not self.enabled: - return - except AttributeError: - pass - import json - - _LOGGER.debug("%s: Refreshing %s", self.account, self.name) - state = None - state_json = await self.alexa_api.get_guard_state( - self._login, self._appliance_id - ) - # _LOGGER.debug("%s: state_json %s", self.account, state_json) - if state_json and "deviceStates" in state_json and state_json["deviceStates"]: - cap = state_json["deviceStates"][0]["capabilityStates"] - # _LOGGER.debug("%s: cap %s", self.account, cap) - for item_json in cap: - item = json.loads(item_json) - # _LOGGER.debug("%s: item %s", self.account, item) - if item["name"] == "armState": - state = item["value"] - # _LOGGER.debug("%s: state %s", self.account, state) - elif state_json["errors"]: - _LOGGER.debug( - "%s: Error refreshing alarm_control_panel %s: %s", - self.account, - self.name, - json.dumps(state_json["errors"]) if state_json else None, - ) - if state is None: - return - if state == "ARMED_AWAY": - self._state = STATE_ALARM_ARMED_AWAY - elif state == "ARMED_STAY": - self._state = STATE_ALARM_DISARMED - else: - self._state = STATE_ALARM_DISARMED - _LOGGER.debug("%s: Alarm State: %s", self.account, self.state) - self.async_schedule_update_ha_state() - - @_catch_login_errors - async def async_alarm_disarm(self, code=None) -> None: - # pylint: disable=unexpected-keyword-arg - """Send disarm command.""" - try: - if not self.enabled: - return - except AttributeError: - pass - await self.alexa_api.set_guard_state( - self._login, self._guard_entity_id, "ARMED_STAY" - ) - await self.async_update(no_throttle=True) - self.async_schedule_update_ha_state() - - @_catch_login_errors - async def async_alarm_arm_away(self, code=None) -> None: - """Send arm away command.""" - # pylint: disable=unexpected-keyword-arg - try: - if not self.enabled: - return - except AttributeError: - pass - await self.alexa_api.set_guard_state( - self._login, self._guard_entity_id, "ARMED_AWAY" - ) - await self.async_update(no_throttle=True) - self.async_schedule_update_ha_state() - - @property - def unique_id(self): - """Return the unique ID.""" - return self._guard_entity_id - - @property - def name(self): - """Return the name of the device.""" - return self._friendly_name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attrs - - @property - def should_poll(self): - """Return the polling state.""" - return self._should_poll or not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ) - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - try: - from homeassistant.components.alarm_control_panel import ( - SUPPORT_ALARM_ARM_AWAY, - ) - except ImportError: - return 0 - return SUPPORT_ALARM_ARM_AWAY diff --git a/custom_components/alexa_media/config_flow.py b/custom_components/alexa_media/config_flow.py deleted file mode 100644 index ab85b8b..0000000 --- a/custom_components/alexa_media/config_flow.py +++ /dev/null @@ -1,378 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Alexa Config Flow. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -from collections import OrderedDict -import logging -from typing import Text - -from alexapy import AlexapyConnectionError -from homeassistant import config_entries -from homeassistant.const import ( - CONF_EMAIL, - CONF_NAME, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_URL, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -import voluptuous as vol - -from .const import ( - CONF_DEBUG, - CONF_EXCLUDE_DEVICES, - CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA, - DOMAIN, -) - -_LOGGER = logging.getLogger(__name__) - - -@callback -def configured_instances(hass): - """Return a set of configured Alexa Media instances.""" - return set(entry.title for entry in hass.config_entries.async_entries(DOMAIN)) - - -@config_entries.HANDLERS.register(DOMAIN) -class AlexaMediaFlowHandler(config_entries.ConfigFlow): - """Handle a Alexa Media config flow.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - - def _update_ord_dict(self, old_dict: OrderedDict, new_dict: dict) -> OrderedDict: - result: OrderedDict = OrderedDict() - for k, v in old_dict.items(): - for key, value in new_dict.items(): - if k == key: - # _LOGGER.debug( - # "Replacing (%s:default(%s), %s) with (%s:default(%s), %s)", - # k, - # k.default() if hasattr(k.default, '__call__') else "", - # v, - # key, - # key.default() if hasattr(key.default, '__call__') else "", - # value, - # ) - result.update([(key, value)]) - break - if k not in result: - # _LOGGER.debug("Keeping (%s, %s)", k, v) - result.update([(k, v)]) - return result - - def __init__(self): - """Initialize the config flow.""" - self.login = None - self.config = OrderedDict() - self.data_schema = OrderedDict( - [ - (vol.Required(CONF_EMAIL), str), - (vol.Required(CONF_PASSWORD), str), - (vol.Required(CONF_URL, default="amazon.com"), str), - (vol.Optional(CONF_DEBUG, default=False), bool), - (vol.Optional(CONF_INCLUDE_DEVICES, default=""), str), - (vol.Optional(CONF_EXCLUDE_DEVICES, default=""), str), - (vol.Optional(CONF_SCAN_INTERVAL, default=60), int), - ] - ) - self.captcha_schema = OrderedDict( - [(vol.Required(CONF_PASSWORD), str), (vol.Required("captcha"), str)] - ) - self.twofactor_schema = OrderedDict([(vol.Required("securitycode"), str)]) - self.claimspicker_schema = OrderedDict([(vol.Required("claimsoption"), str)]) - self.authselect_schema = OrderedDict([(vol.Required("authselectoption"), int)]) - self.verificationcode_schema = OrderedDict( - [(vol.Required("verificationcode"), str)] - ) - - async def _show_form( - self, step="user", placeholders=None, errors=None, data_schema=None - ) -> None: - """Show the form to the user.""" - _LOGGER.debug("show_form %s %s %s %s", step, placeholders, errors, data_schema) - data_schema = data_schema or vol.Schema(self.data_schema) - if step == "user": - return self.async_show_form( - step_id=step, - data_schema=data_schema, - errors=errors if errors else {}, - description_placeholders=placeholders if placeholders else {}, - ) - elif step == "captcha": - return self.async_show_form( - step_id=step, - data_schema=data_schema, - errors={}, - description_placeholders=placeholders if placeholders else {}, - ) - elif step == "twofactor": - return self.async_show_form( - step_id=step, - data_schema=data_schema, - errors={}, - description_placeholders=placeholders if placeholders else {}, - ) - elif step == "claimspicker": - return self.async_show_form( - step_id=step, - data_schema=data_schema, - errors=errors if errors else {}, - description_placeholders=placeholders if placeholders else {}, - ) - elif step == "authselect": - return self.async_show_form( - step_id=step, - data_schema=data_schema, - errors=errors if errors else {}, - description_placeholders=placeholders if placeholders else {}, - ) - elif step == "verificationcode": - return self.async_show_form( - step_id=step, - data_schema=data_schema, - errors=errors if errors else {}, - description_placeholders=placeholders if placeholders else {}, - ) - - async def async_step_import(self, import_config): - """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_config) - - async def async_step_user(self, user_input=None): - """Handle the start of the config flow.""" - from alexapy import AlexaLogin - - if not user_input: - return await self._show_form(data_schema=vol.Schema(self.data_schema)) - - if "{} - {}".format( - user_input[CONF_EMAIL], user_input[CONF_URL] - ) in configured_instances(self.hass): - return await self._show_form(errors={CONF_EMAIL: "identifier_exists"}) - - self.config[CONF_EMAIL] = user_input[CONF_EMAIL] - self.config[CONF_PASSWORD] = user_input[CONF_PASSWORD] - self.config[CONF_URL] = user_input[CONF_URL] - self.config[CONF_DEBUG] = user_input[CONF_DEBUG] - from datetime import timedelta - - self.config[CONF_SCAN_INTERVAL] = ( - user_input[CONF_SCAN_INTERVAL] - if not isinstance(user_input[CONF_SCAN_INTERVAL], timedelta) - else user_input[CONF_SCAN_INTERVAL].total_seconds() - ) - if isinstance(user_input[CONF_INCLUDE_DEVICES], str): - self.config[CONF_INCLUDE_DEVICES] = ( - user_input[CONF_INCLUDE_DEVICES].split(",") - if CONF_INCLUDE_DEVICES in user_input - and user_input[CONF_INCLUDE_DEVICES] != "" - else [] - ) - else: - self.config[CONF_INCLUDE_DEVICES] = user_input[CONF_INCLUDE_DEVICES] - if isinstance(user_input[CONF_EXCLUDE_DEVICES], str): - self.config[CONF_EXCLUDE_DEVICES] = ( - user_input[CONF_EXCLUDE_DEVICES].split(",") - if CONF_EXCLUDE_DEVICES in user_input - and user_input[CONF_EXCLUDE_DEVICES] != "" - else [] - ) - else: - self.config[CONF_EXCLUDE_DEVICES] = user_input[CONF_EXCLUDE_DEVICES] - try: - if not self.login: - _LOGGER.debug("Creating new login") - self.login = AlexaLogin( - self.config[CONF_URL], - self.config[CONF_EMAIL], - self.config[CONF_PASSWORD], - self.hass.config.path, - self.config[CONF_DEBUG], - ) - await self.login.login_with_cookie() - return await self._test_login() - else: - _LOGGER.debug("Using existing login") - await self.login.login(data=user_input) - return await self._test_login() - except AlexapyConnectionError: - return await self._show_form(errors={"base": "connection_error"}) - except BaseException as ex: - _LOGGER.warning("Unknown error: %s", ex) - return await self._show_form(errors={"base": "unknown_error"}) - - async def async_step_captcha(self, user_input=None): - """Handle the input processing of the config flow.""" - return await self.async_step_process(user_input) - - async def async_step_twofactor(self, user_input=None): - """Handle the input processing of the config flow.""" - return await self.async_step_process(user_input) - - async def async_step_claimspicker(self, user_input=None): - """Handle the input processing of the config flow.""" - return await self.async_step_process(user_input) - - async def async_step_authselect(self, user_input=None): - """Handle the input processing of the config flow.""" - return await self.async_step_process(user_input) - - async def async_step_verificationcode(self, user_input=None): - """Handle the input processing of the config flow.""" - return await self.async_step_process(user_input) - - async def async_step_process(self, user_input=None): - """Handle the input processing of the config flow.""" - if user_input: - if CONF_PASSWORD in user_input: - password = user_input[CONF_PASSWORD] - self.config[CONF_PASSWORD] = password - try: - await self.login.login(data=user_input) - except AlexapyConnectionError: - return await self._show_form(errors={"base": "connection_error"}) - except BaseException as ex: - _LOGGER.warning("Unknown error: %s", ex) - return await self._show_form(errors={"base": "unknown_error"}) - return await self._test_login() - - async def _test_login(self): - login = self.login - config = self.config - _LOGGER.debug("Testing login status: %s", login.status) - if "login_successful" in login.status and login.status["login_successful"]: - _LOGGER.debug("Setting up Alexa devices with %s", dict(config)) - await login.close() - return self.async_create_entry( - title="{} - {}".format(login.email, login.url), data=config - ) - if "captcha_required" in login.status and login.status["captcha_required"]: - new_schema = self._update_ord_dict( - self.captcha_schema, - {vol.Required(CONF_PASSWORD, default=config[CONF_PASSWORD]): str}, - ) - _LOGGER.debug("Creating config_flow to request captcha") - return await self._show_form( - "captcha", - data_schema=vol.Schema(new_schema), - errors={}, - placeholders={ - "email": login.email, - "url": login.url, - "captcha_image": "[![captcha]({0})]({0})".format( - login.status["captcha_image_url"] - ), - "message": "\n> {0}".format( - login.status["error_message"] - if "error_message" in login.status - else "" - ), - }, - ) - elif ( - "securitycode_required" in login.status - and login.status["securitycode_required"] - ): - _LOGGER.debug("Creating config_flow to request 2FA") - message = "> {0}".format( - login.status["error_message"] if "error_message" in login.status else "" - ) - return await self._show_form( - "twofactor", - data_schema=vol.Schema(self.twofactor_schema), - errors={}, - placeholders={ - "email": login.email, - "url": login.url, - "message": message, - }, - ) - elif ( - "claimspicker_required" in login.status - and login.status["claimspicker_required"] - ): - error_message = "> {0}".format( - login.status["error_message"] if "error_message" in login.status else "" - ) - _LOGGER.debug("Creating config_flow to select verification method") - claimspicker_message = login.status["claimspicker_message"] - return await self._show_form( - "claimspicker", - data_schema=vol.Schema(self.claimspicker_schema), - errors={}, - placeholders={ - "email": login.email, - "url": login.url, - "message": "> {0}\n> {1}".format( - claimspicker_message, error_message - ), - }, - ) - elif ( - "authselect_required" in login.status - and login.status["authselect_required"] - ): - _LOGGER.debug("Creating config_flow to select OTA method") - error_message = ( - login.status["error_message"] if "error_message" in login.status else "" - ) - authselect_message = login.status["authselect_message"] - return await self._show_form( - "authselect", - data_schema=vol.Schema(self.authselect_schema), - placeholders={ - "email": login.email, - "url": login.url, - "message": "> {0}\n> {1}".format(authselect_message, error_message), - }, - ) - elif ( - "verificationcode_required" in login.status - and login.status["verificationcode_required"] - ): - _LOGGER.debug("Creating config_flow to enter verification code") - return await self._show_form( - "verificationcode", data_schema=vol.Schema(self.verificationcode_schema) - ) - elif "login_failed" in login.status and login.status["login_failed"]: - _LOGGER.debug("Login failed") - return self.async_abort(reason="Login failed") - new_schema = self._update_ord_dict( - self.data_schema, - { - vol.Required(CONF_EMAIL, default=config[CONF_EMAIL]): str, - vol.Required(CONF_PASSWORD, default=config[CONF_PASSWORD]): str, - vol.Required(CONF_URL, default=config[CONF_URL]): str, - vol.Optional(CONF_DEBUG, default=config[CONF_DEBUG]): bool, - vol.Optional( - CONF_INCLUDE_DEVICES, - default=( - config[CONF_INCLUDE_DEVICES] - if isinstance(config[CONF_INCLUDE_DEVICES], str) - else ",".join(map(str, config[CONF_INCLUDE_DEVICES])) - ), - ): str, - vol.Optional( - CONF_EXCLUDE_DEVICES, - default=( - config[CONF_EXCLUDE_DEVICES] - if isinstance(config[CONF_EXCLUDE_DEVICES], str) - else ",".join(map(str, config[CONF_EXCLUDE_DEVICES])) - ), - ): str, - vol.Optional( - CONF_SCAN_INTERVAL, default=config[CONF_SCAN_INTERVAL] - ): int, - }, - ) - return await self._show_form(data_schema=vol.Schema(new_schema)) diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py deleted file mode 100644 index 5374980..0000000 --- a/custom_components/alexa_media/const.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Support to interface with Alexa Devices. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -from datetime import timedelta - -__version__ = "2.5.8" -PROJECT_URL = "https://github.com/custom-components/alexa_media_player/" -ISSUE_URL = "{}issues".format(PROJECT_URL) - -DOMAIN = "alexa_media" -DATA_ALEXAMEDIA = "alexa_media" - -PLAY_SCAN_INTERVAL = 20 -SCAN_INTERVAL = timedelta(seconds=60) -MIN_TIME_BETWEEN_SCANS = SCAN_INTERVAL -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) - -ALEXA_COMPONENTS = ["media_player", "notify", "alarm_control_panel", "switch", "sensor"] - -CONF_ACCOUNTS = "accounts" -CONF_DEBUG = "debug" -CONF_INCLUDE_DEVICES = "include_devices" -CONF_EXCLUDE_DEVICES = "exclude_devices" - -SERVICE_CLEAR_HISTORY = "clear_history" -SERVICE_UPDATE_LAST_CALLED = "update_last_called" -ATTR_MESSAGE = "message" -ATTR_EMAIL = "email" -ATTR_NUM_ENTRIES = "entries" -STARTUP = """ -------------------------------------------------------------------- -{} -Version: {} -This is a custom component -If you have any issues with this you need to open an issue here: -{} -------------------------------------------------------------------- -""".format( - DOMAIN, __version__, ISSUE_URL -) diff --git a/custom_components/alexa_media/helpers.py b/custom_components/alexa_media/helpers.py deleted file mode 100644 index dd01613..0000000 --- a/custom_components/alexa_media/helpers.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Helper functions for Alexa Media Player. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" - -import logging -from typing import Any, Callable, List, Text - -from alexapy import AlexapyLoginError, hide_email -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_component import EntityComponent - -from . import DATA_ALEXAMEDIA - -_LOGGER = logging.getLogger(__name__) - - -async def add_devices( - account: Text, - devices: List[EntityComponent], - add_devices_callback: Callable, - include_filter: List[Text] = [], - exclude_filter: List[Text] = [], -) -> bool: - """Add devices using add_devices_callback.""" - new_devices = [] - for device in devices: - if ( - include_filter - and device.name not in include_filter - or exclude_filter - and device.name in exclude_filter - ): - _LOGGER.debug("%s: Excluding device: %s", account, device) - continue - new_devices.append(device) - devices = new_devices - if devices: - _LOGGER.debug("%s: Adding %s", account, devices) - try: - add_devices_callback(devices, False) - return True - except HomeAssistantError as exception_: - message = exception_.message # type: str - if message.startswith("Entity id already exists"): - _LOGGER.debug("%s: Device already added: %s", account, message) - else: - _LOGGER.debug( - "%s: Unable to add devices: %s : %s", account, devices, message - ) - except BaseException as ex: - template = "An exception of type {0} occurred." " Arguments:\n{1!r}" - message = template.format(type(ex).__name__, ex.args) - _LOGGER.debug("%s: Unable to add devices: %s", account, message) - else: - return True - return False - - -def retry_async( - limit: int = 5, delay: float = 1, catch_exceptions: bool = True -) -> Callable: - """Wrap function with retry logic. - - The function will retry until true or the limit is reached. It will delay - for the period of time specified exponentialy increasing the delay. - - Parameters - ---------- - limit : int - The max number of retries. - delay : float - The delay in seconds between retries. - catch_exceptions : bool - Whether exceptions should be caught and treated as failures or thrown. - Returns - ------- - def - Wrapped function. - - """ - - def wrap(func) -> Callable: - import functools - import asyncio - - @functools.wraps(func) - async def wrapper(*args, **kwargs) -> Any: - _LOGGER.debug( - "%s.%s: Trying with limit %s delay %s catch_exceptions %s", - func.__module__[func.__module__.find(".") + 1 :], - func.__name__, - limit, - delay, - catch_exceptions, - ) - retries: int = 0 - result: bool = False - next_try: int = 0 - while not result and retries < limit: - if retries != 0: - next_try = delay * 2 ** retries - await asyncio.sleep(next_try) - retries += 1 - try: - result = await func(*args, **kwargs) - except Exception as ex: # pylint: disable=broad-except - if not catch_exceptions: - raise - template = "An exception of type {0} occurred." " Arguments:\n{1!r}" - message = template.format(type(ex).__name__, ex.args) - _LOGGER.debug( - "%s.%s: failure caught due to exception: %s", - func.__module__[func.__module__.find(".") + 1 :], - func.__name__, - message, - ) - _LOGGER.debug( - "%s.%s: Try: %s/%s after waiting %s seconds result: %s", - func.__module__[func.__module__.find(".") + 1 :], - func.__name__, - retries, - limit, - next_try, - result, - ) - return result - - return wrapper - - return wrap - - -def _catch_login_errors(func) -> Callable: - """Detect AlexapyLoginError and attempt relogin.""" - import functools - - @functools.wraps(func) - async def wrapper(*args, **kwargs) -> Any: - try: - result = await func(*args, **kwargs) - except AlexapyLoginError as ex: # pylint: disable=broad-except - template = "An exception of type {0} occurred." " Arguments:\n{1!r}" - message = template.format(type(ex).__name__, ex.args) - _LOGGER.debug( - "%s.%s: detected bad login: %s", - func.__module__[func.__module__.find(".") + 1 :], - func.__name__, - message, - ) - instance = args[0] - if hasattr(instance, "_login"): - login = instance._login - email = login.email - hass = instance.hass if instance.hass else None - if hass and ( - "configurator" - not in (hass.data[DATA_ALEXAMEDIA]["accounts"][email]) - or not ( - hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"] - ) - ): - config_entry = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ - "config_entry" - ] - callback = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ - "setup_platform_callback" - ] - test_login_status = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ - "test_login_status" - ] - _LOGGER.debug( - "%s: Alexa API disconnected; attempting to relogin", - hide_email(email), - ) - await login.login_with_cookie() - await test_login_status(hass, config_entry, login, callback) - return None - return result - - return wrapper diff --git a/custom_components/alexa_media/manifest.json b/custom_components/alexa_media/manifest.json deleted file mode 100644 index 18ffea2..0000000 --- a/custom_components/alexa_media/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "domain": "alexa_media", - "name": "Alexa Media Player", - "config_flow": true, - "documentation": "https://github.com/custom-components/alexa_media_player/wiki", - "dependencies": [], - "codeowners": [ - "keatontaylor", - "alandtse" - ], - "requirements": [ - "alexapy==1.5.2" - ], - "homeassistant": "0.96.0" -} \ No newline at end of file diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py deleted file mode 100644 index 0bfc81d..0000000 --- a/custom_components/alexa_media/media_player.py +++ /dev/null @@ -1,1084 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Support to interface with Alexa Devices. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -import asyncio -import logging -import re -from typing import List, Text # noqa pylint: disable=unused-import - -from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, - SUPPORT_SHUFFLE_SET, - SUPPORT_STOP, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, -) -from homeassistant.const import ( - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, - STATE_UNAVAILABLE, -) -from homeassistant.helpers.event import async_call_later - -from . import ( - CONF_EMAIL, - DATA_ALEXAMEDIA, - DOMAIN as ALEXA_DOMAIN, - MIN_TIME_BETWEEN_FORCED_SCANS, - MIN_TIME_BETWEEN_SCANS, - hide_email, - hide_serial, -) -from .const import PLAY_SCAN_INTERVAL -from .helpers import _catch_login_errors, add_devices, retry_async - -SUPPORT_ALEXA = ( - SUPPORT_PAUSE - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_STOP - | SUPPORT_VOLUME_SET - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_PAUSE - | SUPPORT_SELECT_SOURCE - | SUPPORT_SHUFFLE_SET -) -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = [ALEXA_DOMAIN] - - -@retry_async(limit=5, delay=2, catch_exceptions=True) -async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): - # pylint: disable=unused-argument - """Set up the Alexa media player platform.""" - devices = [] # type: List[AlexaClient] - account = config[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - for key, device in account_dict["devices"]["media_player"].items(): - if key not in account_dict["entities"]["media_player"]: - alexa_client = AlexaClient(device, account_dict["login_obj"]) - await alexa_client.init(device) - devices.append(alexa_client) - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ - "media_player" - ][key] - ) = alexa_client - else: - _LOGGER.debug( - "%s: Skipping already added device: %s:%s", - hide_email(account), - hide_serial(key), - alexa_client, - ) - return await add_devices(hide_email(account), devices, add_devices_callback) - - -async def async_setup_entry(hass, config_entry, async_add_devices): - """Set up the Alexa media player platform by config_entry.""" - return await async_setup_platform( - hass, config_entry.data, async_add_devices, discovery_info=None - ) - - -async def async_unload_entry(hass, entry) -> bool: - """Unload a config entry.""" - account = entry.data[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - for device in account_dict["entities"]["media_player"].values(): - await device.async_remove() - return True - - -class AlexaClient(MediaPlayerDevice): - """Representation of a Alexa device.""" - - def __init__(self, device, login): - # pylint: disable=unused-argument - """Initialize the Alexa device.""" - from alexapy import AlexaAPI - - # Class info - self._login = login - self.alexa_api = AlexaAPI(self, login) - self.auth = None - self.alexa_api_session = login.session - self.account = hide_email(login.email) - - # Logged in info - self._authenticated = None - self._can_access_prime_music = None - self._customer_email = None - self._customer_id = None - self._customer_name = None - - # Device info - self._device_name = None - self._device_serial_number = None - self._device_type = None - self._device_family = None - self._device_owner_customer_id = None - self._software_version = None - self._available = None - self._capabilities = [] - self._cluster_members = [] - self._locale = None - # Media - self._session = None - self._media_duration = None - self._media_image_url = None - self._media_title = None - self._media_pos = None - self._media_album_name = None - self._media_artist = None - self._media_player_state = None - self._media_is_muted = None - self._media_vol_level = None - self._previous_volume = None - self._source = None - self._source_list = [] - self._shuffle = None - self._repeat = None - self._playing_parent = None - # Last Device - self._last_called = None - self._last_called_timestamp = None - # Do not Disturb state - self._dnd = None - # Polling state - self._should_poll = True - self._last_update = util.utcnow() - self._listener = None - self._bluetooth_state = None - self._app_device_list = None - self._parent_clusters = None - self._timezone = None - - async def init(self, device): - """Initialize.""" - await self.refresh(device) - - async def async_added_to_hass(self): - """Perform tasks after loading.""" - # Register event handler on bus - self._listener = self.hass.bus.async_listen( - f"{ALEXA_DOMAIN}_{hide_email(self._login.email)}"[0:32], self._handle_event - ) - - async def async_will_remove_from_hass(self): - """Prepare to remove entity.""" - # Register event handler on bus - self._listener() - - async def _handle_event(self, event): - """Handle events. - - This will update last_called and player_state events. - Each MediaClient reports if it's the last_called MediaClient and will - listen for HA events to determine it is the last_called. - When polling instead of websockets, all devices on same account will - update to handle starting music with other devices. If websocket is on - only the updated alexa will update. - Last_called events are only sent if it's a new device or timestamp. - Without polling, we must schedule the HA update manually. - https://developers.home-assistant.io/docs/en/entity_index.html#subscribing-to-updates - The difference between self.update and self.schedule_update_ha_state - is self.update will pull data from Amazon, while schedule_update - assumes the MediaClient state is already updated. - """ - - async def _refresh_if_no_audiopush(already_refreshed=False): - email = self._login.email - seen_commands = ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][email][ - "websocket_commands" - ].keys() - if "websocket_commands" - in (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]) - else None - ) - if ( - not already_refreshed - and seen_commands - and not ( - "PUSH_AUDIO_PLAYER_STATE" in seen_commands - or "PUSH_MEDIA_CHANGE" in seen_commands - or "PUSH_MEDIA_PROGRESS_CHANGE" in seen_commands - ) - ): - # force refresh if player_state update not found, see #397 - _LOGGER.debug( - "%s: No PUSH_AUDIO_PLAYER_STATE/" - "PUSH_MEDIA_CHANGE/PUSH_MEDIA_PROGRESS_CHANGE in %s;" - "forcing refresh", - hide_email(email), - seen_commands, - ) - await self.async_update() - - try: - if not self.enabled: - return - except AttributeError: - pass - already_refreshed = False - event_serial = None - if "last_called_change" in event.data: - event_serial = ( - event.data["last_called_change"]["serialNumber"] - if event.data["last_called_change"] - else None - ) - elif "bluetooth_change" in event.data: - event_serial = ( - event.data["bluetooth_change"]["deviceSerialNumber"] - if event.data["bluetooth_change"] - else None - ) - elif "player_state" in event.data: - event_serial = ( - event.data["player_state"]["dopplerId"]["deviceSerialNumber"] - if event.data["player_state"] - else None - ) - elif "queue_state" in event.data: - event_serial = ( - event.data["queue_state"]["dopplerId"]["deviceSerialNumber"] - if event.data["queue_state"] - else None - ) - if not event_serial: - return - self.available = True - self.async_schedule_update_ha_state() - if "last_called_change" in event.data: - if event_serial == self.device_serial_number or any( - item["serialNumber"] == event_serial for item in self._app_device_list - ): - _LOGGER.debug( - "%s is last_called: %s", - self.name, - hide_serial(self.device_serial_number), - ) - self._last_called = True - self._last_called_timestamp = event.data["last_called_change"][ - "timestamp" - ] - else: - self._last_called = False - if self.hass and self.async_schedule_update_ha_state: - email = self._login.email - force_refresh = not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] - ) - self.async_schedule_update_ha_state(force_refresh=force_refresh) - elif "bluetooth_change" in event.data: - if event_serial == self.device_serial_number: - _LOGGER.debug( - "%s bluetooth_state update: %s", - self.name, - hide_serial(event.data["bluetooth_change"]), - ) - self._bluetooth_state = event.data["bluetooth_change"] - # the setting of bluetooth_state is not consistent as this - # takes from the event instead of the hass storage. We're - # setting the value twice. Architectually we should have a - # single authorative source of truth. - self._source = await self._get_source() - self._source_list = await self._get_source_list() - if self.hass and self.async_schedule_update_ha_state: - self.async_schedule_update_ha_state() - elif "player_state" in event.data: - player_state = event.data["player_state"] - if event_serial == self.device_serial_number: - if "audioPlayerState" in player_state: - _LOGGER.debug( - "%s state update: %s", - self.name, - player_state["audioPlayerState"], - ) - # allow delay before trying to refresh to avoid http 400 errors - await asyncio.sleep(2) - await self.async_update() - already_refreshed = True - elif "mediaReferenceId" in player_state: - _LOGGER.debug( - "%s media update: %s", - self.name, - player_state["mediaReferenceId"], - ) - await self.async_update() - already_refreshed = True - elif "volumeSetting" in player_state: - _LOGGER.debug( - "%s volume updated: %s", - self.name, - player_state["volumeSetting"], - ) - self._media_vol_level = player_state["volumeSetting"] / 100 - if self.hass and self.async_schedule_update_ha_state: - self.async_schedule_update_ha_state() - elif "dopplerConnectionState" in player_state: - self.available = player_state["dopplerConnectionState"] == "ONLINE" - if self.hass and self.async_schedule_update_ha_state: - self.async_schedule_update_ha_state() - await _refresh_if_no_audiopush(already_refreshed) - if "queue_state" in event.data: - queue_state = event.data["queue_state"] - if event_serial == self.device_serial_number: - if ( - "trackOrderChanged" in queue_state - and not queue_state["trackOrderChanged"] - and "loopMode" in queue_state - ): - self._repeat = queue_state["loopMode"] == "LOOP_QUEUE" - _LOGGER.debug( - "%s repeat updated to: %s %s", - self.name, - self._repeat, - queue_state["loopMode"], - ) - elif "playBackOrder" in queue_state: - self._shuffle = queue_state["playBackOrder"] == "SHUFFLE_ALL" - _LOGGER.debug( - "%s shuffle updated to: %s %s", - self.name, - self._shuffle, - queue_state["playBackOrder"], - ) - await _refresh_if_no_audiopush(already_refreshed) - - async def _clear_media_details(self): - """Set all Media Items to None.""" - # General - self._media_duration = None - self._media_image_url = None - self._media_title = None - self._media_pos = None - self._media_album_name = None - self._media_artist = None - self._media_player_state = None - self._media_is_muted = None - # volume is also used for announce/tts so state should remain - # self._media_vol_level = None - - async def _set_authentication_details(self, auth): - """Set Authentication based off auth.""" - self._authenticated = auth["authenticated"] - self._can_access_prime_music = auth["canAccessPrimeMusicContent"] - self._customer_email = auth["customerEmail"] - self._customer_id = auth["customerId"] - self._customer_name = auth["customerName"] - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - @_catch_login_errors - async def refresh(self, device=None, skip_api: bool = False): - """Refresh device data. - - This is a per device refresh and for many Alexa devices can result in - many refreshes from each individual device. This will call the - AlexaAPI directly. - - Args: - device (json): A refreshed device json from Amazon. For efficiency, - an individual device does not refresh if it's reported - as offline. - no_api (bool): Whether to only due a device json update and not hit the API - - """ - if device is not None: - self._device_name = device["accountName"] - self._device_family = device["deviceFamily"] - self._device_type = device["deviceType"] - self._device_serial_number = device["serialNumber"] - self._app_device_list = device["appDeviceList"] - self._device_owner_customer_id = device["deviceOwnerCustomerId"] - self._software_version = device["softwareVersion"] - self._available = device["online"] - self._capabilities = device["capabilities"] - self._cluster_members = device["clusterMembers"] - self._parent_clusters = device["parentClusters"] - self._bluetooth_state = device["bluetooth_state"] - self._locale = device["locale"] if "locale" in device else "en-US" - self._timezone = device["timeZoneId"] if "timeZoneId" in device else "UTC" - self._dnd = device["dnd"] if "dnd" in device else None - await self._set_authentication_details(device["auth_info"]) - session = None - if self.available: - _LOGGER.debug("%s: Refreshing %s", self.account, self.name) - if "PAIR_BT_SOURCE" in self._capabilities: - self._source = await self._get_source() - self._source_list = await self._get_source_list() - self._last_called = await self._get_last_called() - if self._last_called: - self._last_called_timestamp = self.hass.data[DATA_ALEXAMEDIA][ - "accounts" - ][self._login.email]["last_called"]["timestamp"] - if skip_api: - return - if "MUSIC_SKILL" in self._capabilities: - if self._parent_clusters and self.hass: - playing_parents = list( - filter( - lambda x: ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][ - self._login.email - ]["entities"]["media_player"].get(x) - and self.hass.data[DATA_ALEXAMEDIA]["accounts"][ - self._login.email - ]["entities"]["media_player"][x].state - == STATE_PLAYING - ), - self._parent_clusters, - ) - ) - else: - playing_parents = [] - parent_session = {} - if playing_parents: - if len(playing_parents) > 1: - _LOGGER.warning( - "Found multiple playing parents " "please file an issue" - ) - parent = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ - self._login.email - ]["entities"]["media_player"][playing_parents[0]] - self._playing_parent = parent - parent_session = parent.session - if parent_session: - session = parent_session.copy() - session["isPlayingInLemur"] = False - session["lemurVolume"] = None - session["volume"] = ( - parent_session["lemurVolume"]["memberVolume"][ - self.device_serial_number - ] - if parent_session.get("lemurVolume") - and parent_session.get("lemurVolume", {}) - .get("memberVolume", {}) - .get(self.device_serial_number) - else session["volume"] - ) - session = {"playerInfo": session} - else: - self._playing_parent = None - session = await self.alexa_api.get_state() - await self._clear_media_details() - # update the session if it exists - self._session = session if session else None - if self._session and self._session.get("playerInfo"): - self._session = self._session["playerInfo"] - if self._session.get("transport"): - self._shuffle = ( - self._session["transport"]["shuffle"] == "SELECTED" - if ( - "shuffle" in self._session["transport"] - and self._session["transport"]["shuffle"] != "DISABLED" - ) - else None - ) - self._repeat = ( - self._session["transport"]["repeat"] == "SELECTED" - if ( - "repeat" in self._session["transport"] - and self._session["transport"]["repeat"] != "DISABLED" - ) - else None - ) - if self._session.get("state"): - self._media_player_state = self._session["state"] - self._media_pos = self._session.get("progress", {}).get("mediaProgress") - self._media_title = self._session.get("infoText", {}).get("title") - self._media_artist = self._session.get("infoText", {}).get("subText1") - self._media_album_name = self._session.get("infoText", {}).get( - "subText2" - ) - self._media_image_url = self._session.get("mainArt", {}).get("url") - self._media_duration = self._session.get("progress", {}).get( - "mediaLength" - ) - if not self._session.get("lemurVolume"): - self._media_is_muted = ( - self._session.get("volume", {}).get("muted") - if self._session.get("volume") - else self._media_is_muted - ) - self._media_vol_level = ( - self._session["volume"]["volume"] / 100 - if self._session.get("volume") - and self._session.get("volume", {}).get("volume") - else self._media_vol_level - ) - else: - self._media_is_muted = ( - self._session.get("lemurVolume", {}) - .get("compositeVolume", {}) - .get("muted") - ) - self._media_vol_level = ( - self._session["lemurVolume"]["compositeVolume"]["volume"] / 100 - if ( - self._session.get("lemurVolume", {}) - .get("compositeVolume", {}) - .get("volume") - ) - else self._media_vol_level - ) - if self.hass and self._session.get("isPlayingInLemur"): - asyncio.gather( - *map( - lambda x: ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][ - self._login.email - ]["entities"]["media_player"][x].async_update() - ), - filter( - lambda x: ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][ - self._login.email - ]["entities"]["media_player"].get(x) - and self.hass.data[DATA_ALEXAMEDIA]["accounts"][ - self._login.email - ]["entities"]["media_player"][x].available - ), - self._cluster_members, - ), - ) - ) - - @property - def source(self): - """Return the current input source.""" - return self._source - - @property - def source_list(self): - """List of available input sources.""" - return self._source_list - - @_catch_login_errors - async def async_select_source(self, source): - """Select input source.""" - if source == "Local Speaker": - await self.alexa_api.disconnect_bluetooth() - self._source = "Local Speaker" - elif self._bluetooth_state["pairedDeviceList"] is not None: - for devices in self._bluetooth_state["pairedDeviceList"]: - if devices["friendlyName"] == source: - await self.alexa_api.set_bluetooth(devices["address"]) - self._source = source - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - async def _get_source(self): - source = "Local Speaker" - if self._bluetooth_state.get("pairedDeviceList"): - for device in self._bluetooth_state["pairedDeviceList"]: - if ( - device["connected"] is True - and device["friendlyName"] in self.source_list - ): - return device["friendlyName"] - return source - - async def _get_source_list(self): - sources = [] - if self._bluetooth_state.get("pairedDeviceList"): - for devices in self._bluetooth_state["pairedDeviceList"]: - if devices["profiles"] and "A2DP-SOURCE" in devices["profiles"]: - sources.append(devices["friendlyName"]) - return ["Local Speaker"] + sources - - async def _get_last_called(self): - try: - last_called_serial = ( - None - if self.hass is None - else ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email][ - "last_called" - ]["serialNumber"] - ) - ) - except (TypeError, KeyError): - last_called_serial = None - _LOGGER.debug( - "%s: Last_called check: self: %s reported: %s", - self._device_name, - hide_serial(self._device_serial_number), - hide_serial(last_called_serial), - ) - return last_called_serial is not None and ( - self._device_serial_number == last_called_serial - or any( - item["serialNumber"] == last_called_serial - for item in self._app_device_list - ) - ) - - @property - def available(self): - """Return the availability of the client.""" - return self._available - - @available.setter - def available(self, state): - """Set the availability state.""" - self._available = state - - @property - def hidden(self): - """Return whether the sensor should be hidden.""" - return "MUSIC_SKILL" not in self._capabilities - - @property - def unique_id(self): - """Return the id of this Alexa client.""" - return self.device_serial_number - - @property - def name(self): - """Return the name of the device.""" - return self._device_name - - @property - def device_serial_number(self): - """Return the machine identifier of the device.""" - return self._device_serial_number - - @property - def session(self): - """Return the session, if any.""" - return self._session - - @property - def state(self): - """Return the state of the device.""" - if not self.available: - return STATE_UNAVAILABLE - if self._media_player_state == "PLAYING": - return STATE_PLAYING - if self._media_player_state == "PAUSED": - return STATE_PAUSED - if self._media_player_state == "IDLE": - return STATE_IDLE - return STATE_STANDBY - - @_catch_login_errors - async def async_update(self): - """Get the latest details on a media player. - - Because media players spend the majority of time idle, an adaptive - update should be used to avoid flooding Amazon focusing on known - play states. An initial version included an update_devices call on - every update. However, this quickly floods the network for every new - device added. This should only call refresh() to call the AlexaAPI. - """ - try: - if not self.enabled: - return - except AttributeError: - pass - if self.entity_id is None: - # Device has not initialized yet - return - email = self._login.email - if email not in self.hass.data[DATA_ALEXAMEDIA]["accounts"]: - return - device = self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ - "media_player" - ][self.unique_id] - seen_commands = ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][email][ - "websocket_commands" - ].keys() - if "websocket_commands" - in (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]) - else None - ) - await self.refresh( # pylint: disable=unexpected-keyword-arg - device, no_throttle=True - ) - websocket_enabled = self.hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( - "websocket" - ) - if ( - self.state in [STATE_PLAYING] - and - # only enable polling if websocket not connected - ( - not websocket_enabled - or not seen_commands - or not ( - "PUSH_AUDIO_PLAYER_STATE" in seen_commands - or "PUSH_MEDIA_CHANGE" in seen_commands - or "PUSH_MEDIA_PROGRESS_CHANGE" in seen_commands - ) - ) - ): - self._should_poll = False # disable polling since manual update - if ( - self._last_update == 0 - or util.dt.as_timestamp(util.utcnow()) - - util.dt.as_timestamp(self._last_update) - > PLAY_SCAN_INTERVAL - ): - _LOGGER.debug( - "%s playing; scheduling update in %s seconds", - self.name, - PLAY_SCAN_INTERVAL, - ) - async_call_later( - self.hass, - PLAY_SCAN_INTERVAL, - lambda _: self.async_schedule_update_ha_state(force_refresh=True), - ) - elif self._should_poll: # Not playing, one last poll - self._should_poll = False - if not websocket_enabled: - _LOGGER.debug( - "Disabling polling and scheduling last update in" - " 300 seconds for %s", - self.name, - ) - async_call_later( - self.hass, - 300, - lambda _: self.async_schedule_update_ha_state(force_refresh=True), - ) - else: - _LOGGER.debug("Disabling polling for %s", self.name) - self._last_update = util.utcnow() - self.async_schedule_update_ha_state() - - @property - def media_content_type(self): - """Return the content type of current playing media.""" - if self.state in [STATE_PLAYING, STATE_PAUSED]: - return MEDIA_TYPE_MUSIC - return STATE_STANDBY - - @property - def media_artist(self): - """Return the artist of current playing media, music track only.""" - return self._media_artist - - @property - def media_album_name(self): - """Return the album name of current playing media, music track only.""" - return self._media_album_name - - @property - def media_duration(self): - """Return the duration of current playing media in seconds.""" - return self._media_duration - - @property - def media_position(self): - """Return the duration of current playing media in seconds.""" - return self._media_pos - - @property - def media_position_updated_at(self): - """When was the position of the current playing media valid.""" - return self._last_update - - @property - def media_image_url(self) -> Text: - """Return the image URL of current playing media.""" - if self._media_image_url: - return re.sub("\\(", "%28", re.sub("\\)", "%29", self._media_image_url)) - # fix failure of HA media player ui to quote "(" or ")" - return None - - @property - def media_image_remotely_accessible(self): - """Return whether image is accessible outside of the home network.""" - return bool(self._media_image_url) - - @property - def media_title(self): - """Return the title of current playing media.""" - return self._media_title - - @property - def device_family(self): - """Return the make of the device (ex. Echo, Other).""" - return self._device_family - - @property - def dnd_state(self): - """Return the Do Not Disturb state.""" - return self._dnd - - @dnd_state.setter - def dnd_state(self, state): - """Set the Do Not Disturb state.""" - self._dnd = state - - @_catch_login_errors - async def async_set_shuffle(self, shuffle): - """Enable/disable shuffle mode.""" - await self.alexa_api.shuffle(shuffle) - self._shuffle = shuffle - - @property - def shuffle(self): - """Return the Shuffle state.""" - return self._shuffle - - @shuffle.setter - def shuffle(self, state): - """Set the Shuffle state.""" - self._shuffle = state - self.async_schedule_update_ha_state() - - @property - def repeat_state(self): - """Return the Repeat state.""" - return self._repeat - - @repeat_state.setter - def repeat_state(self, state): - """Set the Repeat state.""" - self._repeat = state - self.async_schedule_update_ha_state() - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_ALEXA - - @_catch_login_errors - async def async_set_volume_level(self, volume): - """Set volume level, range 0..1.""" - if not self.available: - return - await self.alexa_api.set_volume(volume) - self._media_vol_level = volume - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - @property - def volume_level(self): - """Return the volume level of the client (0..1).""" - return self._media_vol_level - - @property - def is_volume_muted(self): - """Return boolean if volume is currently muted.""" - if self.volume_level == 0: - return True - return False - - @_catch_login_errors - async def async_mute_volume(self, mute): - """Mute the volume. - - Since we can't actually mute, we'll: - - On mute, store volume and set volume to 0 - - On unmute, set volume to previously stored volume - """ - if not self.available: - return - - self._media_is_muted = mute - if mute: - self._previous_volume = self.volume_level - await self.alexa_api.set_volume(0) - else: - if self._previous_volume is not None: - await self.alexa_api.set_volume(self._previous_volume) - else: - await self.alexa_api.set_volume(50) - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - @_catch_login_errors - async def async_media_play(self): - """Send play command.""" - if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): - return - if self._playing_parent: - await self._playing_parent.async_media_play() - else: - await self.alexa_api.play() - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - @_catch_login_errors - async def async_media_pause(self): - """Send pause command.""" - if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): - return - if self._playing_parent: - await self._playing_parent.async_media_pause() - else: - await self.alexa_api.pause() - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - @_catch_login_errors - async def async_turn_off(self): - """Turn the client off. - - While Alexa's do not have on/off capability, we can use this as another - trigger to do updates. For turning off, we can clear media_details. - """ - self._should_poll = False - await self.async_media_pause() - await self._clear_media_details() - - @_catch_login_errors - async def async_turn_on(self): - """Turn the client on. - - While Alexa's do not have on/off capability, we can use this as another - trigger to do updates. - """ - self._should_poll = True - await self.async_media_pause() - - @_catch_login_errors - async def async_media_next_track(self): - """Send next track command.""" - if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): - return - if self._playing_parent: - await self._playing_parent.async_media_next_track() - else: - await self.alexa_api.next() - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - @_catch_login_errors - async def async_media_previous_track(self): - """Send previous track command.""" - if not (self.state in [STATE_PLAYING, STATE_PAUSED] and self.available): - return - if self._playing_parent: - await self._playing_parent.async_media_previous_track() - else: - await self.alexa_api.previous() - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - @_catch_login_errors - async def async_send_tts(self, message, **kwargs): - """Send TTS to Device. - - NOTE: Does not work on WHA Groups. - """ - await self.alexa_api.send_tts(message, customer_id=self._customer_id, **kwargs) - - @_catch_login_errors - async def async_send_announcement(self, message, **kwargs): - """Send announcement to the media player.""" - await self.alexa_api.send_announcement( - message, customer_id=self._customer_id, **kwargs - ) - - @_catch_login_errors - async def async_send_mobilepush(self, message, **kwargs): - """Send push to the media player's associated mobile devices.""" - await self.alexa_api.send_mobilepush( - message, customer_id=self._customer_id, **kwargs - ) - - @_catch_login_errors - async def async_play_media(self, media_type, media_id, enqueue=None, **kwargs): - # pylint: disable=unused-argument - """Send the play_media command to the media player.""" - if media_type == "music": - await self.async_send_tts( - "Sorry, text to speech can only be called" - " with the notify.alexa_media service." - " Please see the alexa_media wiki for details." - ) - _LOGGER.warning( - "Sorry, text to speech can only be called" - " with the notify.alexa_media service." - " Please see the alexa_media wiki for details." - "https://github.com/custom-components/alexa_media_player/wiki/Notification-Component#use-the-notifyalexa_media-service" - ) - elif media_type == "sequence": - await self.alexa_api.send_sequence( - media_id, customer_id=self._customer_id, **kwargs - ) - elif media_type == "routine": - await self.alexa_api.run_routine(media_id) - elif media_type == "sound": - await self.alexa_api.play_sound( - media_id, customer_id=self._customer_id, **kwargs - ) - else: - await self.alexa_api.play_music( - media_type, media_id, customer_id=self._customer_id, **kwargs - ) - if not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["websocket"] - ): - await self.async_update() - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = { - "available": self.available, - "last_called": self._last_called, - "last_called_timestamp": self._last_called_timestamp, - } - return attr - - @property - def should_poll(self): - """Return the polling state.""" - return self._should_poll - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(ALEXA_DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Amazon", - "model": f"{self._device_family} {self._device_type}", - "sw_version": self._software_version, - } diff --git a/custom_components/alexa_media/notify.py b/custom_components/alexa_media/notify.py deleted file mode 100644 index 6925ffd..0000000 --- a/custom_components/alexa_media/notify.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Alexa Devices notification service. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -import asyncio -import logging - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - SERVICE_NOTIFY, - BaseNotificationService, -) - -from . import CONF_EMAIL, DATA_ALEXAMEDIA, DOMAIN, hide_email, hide_serial -from .helpers import retry_async - -_LOGGER = logging.getLogger(__name__) - - -@retry_async(limit=5, delay=2, catch_exceptions=True) -async def async_get_service(hass, config, discovery_info=None): - # pylint: disable=unused-argument - """Get the demo notification service.""" - for account, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - for key, _ in account_dict["devices"]["media_player"].items(): - if key not in account_dict["entities"]["media_player"]: - _LOGGER.debug( - "%s: Media player %s not loaded yet; delaying load", - hide_email(account), - hide_serial(key), - ) - return False - return AlexaNotificationService(hass) - - -async def async_unload_entry(hass, entry) -> bool: - """Unload a config entry.""" - target_account = entry.data[CONF_EMAIL] - other_accounts = False - for account, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - if account == target_account: - if "entities" not in account_dict: - continue - for device in account_dict["entities"]["media_player"].values(): - entity_id = device.entity_id.split(".") - hass.services.async_remove(SERVICE_NOTIFY, f"{DOMAIN}_{entity_id[1]}") - else: - other_accounts = True - if not other_accounts: - hass.services.async_remove(SERVICE_NOTIFY, f"{DOMAIN}") - return True - - -class AlexaNotificationService(BaseNotificationService): - """Implement Alexa Media Player notification service.""" - - def __init__(self, hass): - """Initialize the service.""" - self.hass = hass - - def convert(self, names, type_="entities", filter_matches=False): - """Return a list of converted Alexa devices based on names. - - Names may be matched either by serialNumber, accountName, or - Homeassistant entity_id and can return any of the above plus entities - - Parameters - ---------- - names : list(string) - A list of names to convert - type : string - The type to return entities, entity_ids, serialnumbers, names - filter_matches : bool - Whether non-matching items are removed from the returned list. - - Returns - ------- - list(string) - List of home assistant entity_ids - - """ - devices = [] - if isinstance(names, str): - names = [names] - for item in names: - matched = False - for alexa in self.devices: - _LOGGER.debug( - "Testing item: %s against (%s, %s, %s, %s)", - item, - alexa, - alexa.name, - hide_serial(alexa.unique_id), - alexa.entity_id, - ) - if item in (alexa, alexa.name, alexa.unique_id, alexa.entity_id): - if type_ == "entities": - converted = alexa - elif type_ == "serialnumbers": - converted = alexa.unique_id - elif type_ == "names": - converted = alexa.name - elif type_ == "entity_ids": - converted = alexa.entity_id - devices.append(converted) - matched = True - _LOGGER.debug("Converting: %s to (%s): %s", item, type_, converted) - if not filter_matches and not matched: - devices.append(item) - return devices - - @property - def targets(self): - """Return a dictionary of Alexa devices.""" - devices = {} - for _, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - if "devices" not in account_dict: - return devices - for serial, alexa in account_dict["devices"]["media_player"].items(): - devices[alexa["accountName"]] = serial - return devices - - @property - def devices(self): - """Return a list of Alexa devices.""" - devices = [] - if ( - "accounts" not in self.hass.data[DATA_ALEXAMEDIA] - and not self.hass.data[DATA_ALEXAMEDIA]["accounts"].items() - ): - return devices - for _, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items(): - devices = devices + list(account_dict["entities"]["media_player"].values()) - return devices - - async def async_send_message(self, message="", **kwargs): - """Send a message to a Alexa device.""" - _LOGGER.debug("Message: %s, kwargs: %s", message, kwargs) - kwargs["message"] = message - targets = kwargs.get(ATTR_TARGET) - title = kwargs.get(ATTR_TITLE) if ATTR_TITLE in kwargs else ATTR_TITLE_DEFAULT - data = kwargs.get(ATTR_DATA) - if isinstance(targets, str): - targets = [targets] - entities = self.convert(targets, type_="entities") - try: - entities.extend(self.hass.components.group.expand_entity_ids(entities)) - except ValueError: - _LOGGER.debug("Invalid Home Assistant entity in %s", entities) - tasks = [] - if data["type"] == "tts": - targets = self.convert(entities, type_="entities", filter_matches=True) - _LOGGER.debug("TTS entities: %s", targets) - for alexa in targets: - _LOGGER.debug("TTS by %s : %s", alexa, message) - tasks.append(alexa.async_send_tts(message)) - elif data["type"] == "announce": - targets = self.convert(entities, type_="serialnumbers", filter_matches=True) - _LOGGER.debug( - "Announce targets: %s entities: %s", - list(map(hide_serial, targets)), - entities, - ) - for account, account_dict in self.hass.data[DATA_ALEXAMEDIA][ - "accounts" - ].items(): - for alexa in account_dict["entities"]["media_player"].values(): - if alexa.unique_id in targets and alexa.available: - _LOGGER.debug( - ("%s: Announce by %s to " "targets: %s: %s"), - hide_email(account), - alexa, - list(map(hide_serial, targets)), - message, - ) - tasks.append( - alexa.async_send_announcement( - message, - targets=targets, - title=title, - method=(data["method"] if "method" in data else "all"), - ) - ) - break - elif data["type"] == "push": - targets = self.convert(entities, type_="entities", filter_matches=True) - for alexa in targets: - _LOGGER.debug("Push by %s : %s %s", alexa, title, message) - tasks.append(alexa.async_send_mobilepush(message, title=title)) - await asyncio.gather(*tasks) diff --git a/custom_components/alexa_media/sensor.py b/custom_components/alexa_media/sensor.py deleted file mode 100644 index 428a354..0000000 --- a/custom_components/alexa_media/sensor.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Alexa Devices Sensors. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -import datetime -import logging -from typing import List, Text # noqa pylint: disable=unused-import - -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, STATE_UNAVAILABLE -from homeassistant.exceptions import NoEntitySpecifiedError -from homeassistant.helpers.entity import Entity -from homeassistant.util import dt -import pytz - -from . import ( - CONF_EMAIL, - CONF_EXCLUDE_DEVICES, - CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA, - DOMAIN as ALEXA_DOMAIN, - hide_email, - hide_serial, -) -from .helpers import add_devices, retry_async - -_LOGGER = logging.getLogger(__name__) - -LOCAL_TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo - -RECURRING_PATTERN = { - None: "Never Repeat", - "P1D": "Every day", - "XXXX-WE": "Weekends", - "XXXX-WD": "Weekdays", - "XXXX-WXX-1": "Every Monday", - "XXXX-WXX-2": "Every Tuesday", - "XXXX-WXX-3": "Every Wednesday", - "XXXX-WXX-4": "Every Thursday", - "XXXX-WXX-5": "Every Friday", - "XXXX-WXX-6": "Every Saturday", - "XXXX-WXX-7": "Every Sunday", -} - -RECURRING_PATTERN_ISO_SET = { - None: {}, - "P1D": {1, 2, 3, 4, 5, 6, 7}, - "XXXX-WE": {6, 7}, - "XXXX-WD": {1, 2, 3, 4, 5}, - "XXXX-WXX-1": {1}, - "XXXX-WXX-2": {2}, - "XXXX-WXX-3": {3}, - "XXXX-WXX-4": {4}, - "XXXX-WXX-5": {5}, - "XXXX-WXX-6": {6}, - "XXXX-WXX-7": {7}, -} - - -@retry_async(limit=5, delay=5, catch_exceptions=False) -async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Set up the Alexa sensor platform.""" - devices: List[AlexaMediaSensor] = [] - SENSOR_TYPES = { - "Alarm": AlarmSensor, - "Timer": TimerSensor, - "Reminder": ReminderSensor, - } - account = config[CONF_EMAIL] - include_filter = config.get(CONF_INCLUDE_DEVICES, []) - exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - _LOGGER.debug("%s: Loading sensors", hide_email(account)) - if "sensor" not in account_dict["entities"]: - (hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["sensor"]) = {} - for key, device in account_dict["devices"]["media_player"].items(): - if key not in account_dict["entities"]["media_player"]: - _LOGGER.debug( - "%s: Media player %s not loaded yet; delaying load", - hide_email(account), - hide_serial(key), - ) - if devices: - await add_devices( - hide_email(account), - devices, - add_devices_callback, - include_filter, - exclude_filter, - ) - return False - if key not in (account_dict["entities"]["sensor"]): - (account_dict["entities"]["sensor"][key]) = {} - for (n_type, class_) in SENSOR_TYPES.items(): - n_type_dict = ( - account_dict["notifications"][key][n_type] - if key in account_dict["notifications"] - and n_type in account_dict["notifications"][key] - else {} - ) - if ( - n_type in ("Alarm, Timer") - and "TIMERS_AND_ALARMS" in device["capabilities"] - ): - alexa_client = class_( - account_dict["entities"]["media_player"][key], - n_type_dict, - account, - ) - elif n_type in ("Reminder") and "REMINDERS" in device["capabilities"]: - alexa_client = class_( - account_dict["entities"]["media_player"][key], - n_type_dict, - account, - ) - else: - continue - _LOGGER.debug( - "%s: Found %s %s sensor (%s) with next: %s", - hide_email(account), - hide_serial(key), - n_type, - len(n_type_dict.keys()), - alexa_client.state, - ) - devices.append(alexa_client) - (account_dict["entities"]["sensor"][key][n_type]) = alexa_client - else: - for alexa_client in account_dict["entities"]["sensor"][key].values(): - _LOGGER.debug( - "%s: Skipping already added device: %s", - hide_email(account), - alexa_client, - ) - return await add_devices( - hide_email(account), - devices, - add_devices_callback, - include_filter, - exclude_filter, - ) - - -async def async_setup_entry(hass, config_entry, async_add_devices): - """Set up the Alexa sensor platform by config_entry.""" - return await async_setup_platform( - hass, config_entry.data, async_add_devices, discovery_info=None - ) - - -async def async_unload_entry(hass, entry) -> bool: - """Unload a config entry.""" - account = entry.data[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - for key, sensors in account_dict["entities"]["sensor"].items(): - for device in sensors[key].values(): - await device.async_remove() - return True - - -class AlexaMediaSensor(Entity): - """Representation of Alexa Media sensors.""" - - def __init__( - self, - client, - n_dict, - sensor_property: Text, - account, - name="Next Notification", - icon=None, - ): - """Initialize the Alexa sensor device.""" - # Class info - self._client = client - self._n_dict = n_dict - self._sensor_property = sensor_property - self._account = account - self._dev_id = client.unique_id - self._name = name - self._unit = None - self._device_class = DEVICE_CLASS_TIMESTAMP - self._icon = icon - self._all = [] - self._active = [] - self._next = None - self._timestamp: datetime.datetime = None - self._process_raw_notifications() - - def _process_raw_notifications(self): - self._all = ( - list(map(self._fix_alarm_date_time, self._n_dict.items())) - if self._n_dict - else [] - ) - self._all = list(map(self._update_recurring_alarm, self._all)) - self._all = sorted(self._all, key=lambda x: x[1][self._sensor_property]) - self._active = ( - list(filter(lambda x: x[1]["status"] == "ON", self._all)) - if self._all - else [] - ) - self._next = self._active[0][1] if self._active else None - - def _fix_alarm_date_time(self, value): - if ( - self._sensor_property != "date_time" - or not value - or isinstance(value[1][self._sensor_property], datetime.datetime) - ): - return value - naive_time = dt.parse_datetime(value[1][self._sensor_property]) - timezone = pytz.timezone(self._client._timezone) - if timezone and naive_time: - value[1][self._sensor_property] = timezone.localize(naive_time) - elif not naive_time: - # this is typically an older alarm - value[1][self._sensor_property] = datetime.datetime.fromtimestamp( - value[1]["alarmTime"] / 1000, tz=LOCAL_TIMEZONE - ) - _LOGGER.warning( - "There is an old format alarm on %s set for %s. " - " This alarm should be removed in the Alexa app and recreated. ", - self._client.name, - dt.as_local(value[1][self._sensor_property]), - ) - else: - _LOGGER.warning( - "%s is returning erroneous data. " - "Returned times may be wrong. " - "Please confirm the timezone in the Alexa app is correct. " - "Debugging info: \nRaw: %s \nNaive Time: %s " - "\nTimezone: %s", - self._client.name, - value[1], - naive_time, - self._client._timezone, - ) - return value - - def _update_recurring_alarm(self, value): - _LOGGER.debug("value %s", value) - alarm = value[1][self._sensor_property] - alarm_on = value[1]["status"] == "ON" - recurring_pattern = value[1]["recurringPattern"] - while ( - alarm_on - and recurring_pattern - and alarm < dt.now() - and RECURRING_PATTERN_ISO_SET[recurring_pattern] - and alarm.isoweekday not in RECURRING_PATTERN_ISO_SET[recurring_pattern] - ): - alarm += datetime.timedelta(days=1) - if alarm != value[1][self._sensor_property]: - _LOGGER.debug( - "Alarm with recurrence %s set to %s", - RECURRING_PATTERN[recurring_pattern], - alarm, - ) - value[1][self._sensor_property] = alarm - return value - - @staticmethod - def _round_time(value: datetime.datetime) -> datetime.datetime: - precision = datetime.timedelta(seconds=1).total_seconds() - seconds = (value - value.min.replace(tzinfo=value.tzinfo)).seconds - rounding = (seconds + precision / 2) // precision * precision - return value + datetime.timedelta(0, rounding - seconds, -value.microsecond) - - async def async_added_to_hass(self): - """Store register state change callback.""" - try: - if not self.enabled: - return - except AttributeError: - pass - # Register event handler on bus - self._listener = self.hass.bus.async_listen( - f"{ALEXA_DOMAIN}_{hide_email(self._account)}"[0:32], self._handle_event - ) - await self.async_update() - - async def async_will_remove_from_hass(self): - """Prepare to remove entity.""" - # Register event handler on bus - self._listener() - - def _handle_event(self, event): - """Handle events. - - This will update PUSH_NOTIFICATION_CHANGE events to see if the sensor - should be updated. - """ - try: - if not self.enabled: - return - except AttributeError: - pass - if "notification_update" in event.data: - if ( - event.data["notification_update"]["dopplerId"]["deviceSerialNumber"] - == self._client.unique_id - ): - _LOGGER.debug("Updating sensor %s", self.name) - self.async_schedule_update_ha_state(True) - - @property - def available(self): - """Return the availabilty of the sensor.""" - return True - - @property - def hidden(self): - """Return whether the sensor should be hidden.""" - return self.state == STATE_UNAVAILABLE - - @property - def unique_id(self): - """Return the unique ID.""" - return f"{self._client.unique_id}_{self._name}" - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._client.name} {self._name}" - - @property - def should_poll(self): - """Return the polling state.""" - return not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._account]["websocket"] - ) - - @property - def state(self): - """Return the state of the sensor.""" - return ( - self._next[self._sensor_property].replace(tzinfo=LOCAL_TIMEZONE).isoformat() - if self._next - else STATE_UNAVAILABLE - ) - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return self._unit - - @property - def device_class(self): - """Return the device_class of the device.""" - return self._device_class - - async def async_update(self): - """Update state.""" - try: - if not self.enabled: - return - except AttributeError: - pass - account_dict = self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._account] - self._timestamp = account_dict["notifications"]["process_timestamp"] - try: - self._n_dict = account_dict["notifications"][self._dev_id][self._type] - except KeyError: - self._n_dict = None - self._process_raw_notifications() - try: - self.async_schedule_update_ha_state() - except NoEntitySpecifiedError: - pass # we ignore this due to a harmless startup race condition - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(ALEXA_DOMAIN, self._dev_id)}, - "via_device": (ALEXA_DOMAIN, self._dev_id), - } - - @property - def icon(self): - """Return the icon of the sensor.""" - return self._icon - - @property - def recurrence(self): - """Return the recurrence pattern of the sensor.""" - return RECURRING_PATTERN[self._next["recurringPattern"]] if self._next else None - - @property - def device_state_attributes(self): - """Return additional attributes.""" - import json - - attr = { - "recurrence": self.recurrence, - "total_active": len(self._active), - "total_all": len(self._all), - "sorted_active": json.dumps(self._active, default=str), - "sorted_all": json.dumps(self._all, default=str), - } - return attr - - -class AlarmSensor(AlexaMediaSensor): - """Representation of a Alexa Alarm sensor.""" - - def __init__(self, client, n_json, account): - """Initialize the Alexa sensor.""" - # Class info - self._type = "Alarm" - super().__init__( - client, n_json, "date_time", account, f"next {self._type}", "mdi:alarm" - ) - - -class TimerSensor(AlexaMediaSensor): - """Representation of a Alexa Timer sensor.""" - - def __init__(self, client, n_json, account): - """Initialize the Alexa sensor.""" - # Class info - self._type = "Timer" - super().__init__( - client, n_json, "remainingTime", account, f"next {self._type}", "mdi:timer" - ) - - @property - def state(self) -> datetime.datetime: - """Return the state of the sensor.""" - return ( - dt.as_local( - super()._round_time( - datetime.datetime.fromtimestamp( - self._timestamp.timestamp() - + self._next[self._sensor_property] / 1000 - ) - ) - ).isoformat() - if self._next and self._timestamp - else STATE_UNAVAILABLE - ) - - @property - def paused(self) -> bool: - """Return the paused state of the sensor.""" - return self._next["status"] == "PAUSED" if self._next else None - - @property - def icon(self): - """Return the icon of the sensor.""" - return self._icon if not self.paused else "mdi:timer-off" - - -class ReminderSensor(AlexaMediaSensor): - """Representation of a Alexa Reminder sensor.""" - - def __init__(self, client, n_json, account): - """Initialize the Alexa sensor.""" - # Class info - self._type = "Reminder" - super().__init__( - client, n_json, "alarmTime", account, f"next {self._type}", "mdi:reminder" - ) - - @property - def state(self): - """Return the state of the sensor.""" - return ( - dt.as_local( - super()._round_time( - datetime.datetime.fromtimestamp( - self._next[self._sensor_property] / 1000, tz=LOCAL_TIMEZONE - ) - ) - ).isoformat() - if self._next - else STATE_UNAVAILABLE - ) - - @property - def reminder(self): - """Return the reminder of the sensor.""" - return self._next["reminderLabel"] if self._next else None - - @property - def device_state_attributes(self): - """Return the scene state attributes.""" - attr = super().device_state_attributes - attr.update({"reminder": self.reminder}) - return attr diff --git a/custom_components/alexa_media/services.yaml b/custom_components/alexa_media/services.yaml deleted file mode 100644 index c1e4c51..0000000 --- a/custom_components/alexa_media/services.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -update_last_called: - # Description of the service - description: Forces update of last_called echo device for each Alexa account. - # Different fields that your service accepts - fields: - # Key of the field - email: - # Description of the field - description: List of Alexa accounts to update. If empty, will update all known accounts. - # Example value that can be passed for this field - example: 'my_email@alexa.com' - -clear_history: - # Description of the service - description: Clear last entries from Alexa history for each Alexa account. - # Different fields that your service accepts - fields: - # Key of the field - email: - # Description of the field - description: List of Alexa accounts to update. If empty, will delete from all known accounts. - # Example value that can be passed for this field - example: 'my_email@alexa.com' - entries: - # Description of the field - description: Number of entries to clear from 1 to 50. If empty, clear 50. - # Example value that can be passed for this field - example: 50 \ No newline at end of file diff --git a/custom_components/alexa_media/strings.json b/custom_components/alexa_media/strings.json deleted file mode 100644 index 9d81c87..0000000 --- a/custom_components/alexa_media/strings.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "config": { - "error": { - "connection_error": "Error connecting; check network and retry", - "identifier_exists": "Email for Alexa URL already registered", - "invalid_credentials": "Invalid credentials", - "unknown_error": "Unknown error, please report log info" - }, - "step": { - "user": { - "data": { - "password": "Password", - "email": "Email Address", - "url": "Amazon region domain (e.g., amazon.co.uk)", - "include_devices": "Included device (comma separated)", - "exclude_devices": "Excluded device (comma separated)", - "debug": "Advanced debugging", - "scan_interval": "Seconds between scans" - }, - "description": "Please enter your information.", - "title": "Alexa Media Player - Configuration" - }, - "captcha": { - "data": { - "password": "Password", - "captcha": "Captcha" - }, - "description": "**{email} - alexa.{url}**\n{message}\n {captcha_image}", - "title": "Alexa Media Player - Captcha" - }, - "twofactor": { - "data": { - "securitycode": "2FA Code" - }, - "description": "**{email} - alexa.{url}**\nEnter the One Time Password (OTP).\n{message}", - "title": "Alexa Media Player - Two Factor Authentication" - }, - "authselect": { - "data": { - "authselectoption": "OTP method" - }, - "description": "**{email} - alexa.{url}**\n{message}", - "title": "Alexa Media Player - One Time Password" - }, - "claimspicker": { - "data": { - "authselectoption": "Verification method" - }, - "description": "**{email} - alexa.{url}**\nPlease select verification method. (e.g., `sms` or `email`)\n{message}", - "title": "Alexa Media Player - Verification Method" - } - - }, - "title": "Alexa Media Player" - } -} diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py deleted file mode 100644 index 37b3023..0000000 --- a/custom_components/alexa_media/switch.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -""" -Alexa Devices Switches. - -For more details about this platform, please refer to the documentation at -https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 -""" -import logging -from typing import List # noqa pylint: disable=unused-import - -from homeassistant.components.switch import SwitchDevice -from homeassistant.exceptions import NoEntitySpecifiedError - -from . import ( - CONF_EMAIL, - CONF_EXCLUDE_DEVICES, - CONF_INCLUDE_DEVICES, - DATA_ALEXAMEDIA, - DOMAIN as ALEXA_DOMAIN, - hide_email, - hide_serial, -) -from .helpers import _catch_login_errors, add_devices, retry_async - -_LOGGER = logging.getLogger(__name__) - - -@retry_async(limit=5, delay=5, catch_exceptions=True) -async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Set up the Alexa switch platform.""" - devices = [] # type: List[DNDSwitch] - SWITCH_TYPES = [ - ("dnd", DNDSwitch), - ("shuffle", ShuffleSwitch), - ("repeat", RepeatSwitch), - ] - account = config[CONF_EMAIL] - include_filter = config.get(CONF_INCLUDE_DEVICES, []) - exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - _LOGGER.debug("%s: Loading switches", hide_email(account)) - if "switch" not in account_dict["entities"]: - (hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"]) = {} - for key, _ in account_dict["devices"]["media_player"].items(): - if key not in account_dict["entities"]["media_player"]: - _LOGGER.debug( - "%s: Media player %s not loaded yet; delaying load", - hide_email(account), - hide_serial(key), - ) - if devices: - await add_devices( - hide_email(account), - devices, - add_devices_callback, - include_filter, - exclude_filter, - ) - return False - if key not in ( - hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"] - ): - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"][ - key - ] - ) = {} - for (switch_key, class_) in SWITCH_TYPES: - if ( - switch_key == "dnd" - and not account_dict["devices"]["switch"][key].get("dnd") - ) or ( - switch_key in ["shuffle", "repeat"] - and "MUSIC_SKILL" - not in account_dict["devices"]["media_player"][key].get( - "capabilities" - ) - ): - _LOGGER.debug( - "%s: Skipping %s for %s", - hide_email(account), - switch_key, - hide_serial(key), - ) - continue - alexa_client = class_( - account_dict["entities"]["media_player"][key], account - ) # type: AlexaMediaSwitch - _LOGGER.debug( - "%s: Found %s %s switch with status: %s", - hide_email(account), - hide_serial(key), - switch_key, - alexa_client.is_on, - ) - devices.append(alexa_client) - ( - hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ - "switch" - ][key][switch_key] - ) = alexa_client - else: - for alexa_client in hass.data[DATA_ALEXAMEDIA]["accounts"][account][ - "entities" - ]["switch"][key].values(): - _LOGGER.debug( - "%s: Skipping already added device: %s", - hide_email(account), - alexa_client, - ) - return await add_devices( - hide_email(account), - devices, - add_devices_callback, - include_filter, - exclude_filter, - ) - - -async def async_setup_entry(hass, config_entry, async_add_devices): - """Set up the Alexa switch platform by config_entry.""" - return await async_setup_platform( - hass, config_entry.data, async_add_devices, discovery_info=None - ) - - -async def async_unload_entry(hass, entry) -> bool: - """Unload a config entry.""" - account = entry.data[CONF_EMAIL] - account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] - for key, switches in account_dict["entities"]["switch"].items(): - for device in switches[key].values(): - await device.async_remove() - return True - - -class AlexaMediaSwitch(SwitchDevice): - """Representation of a Alexa Media switch.""" - - def __init__(self, client, switch_property, switch_function, account, name="Alexa"): - """Initialize the Alexa Switch device.""" - # Class info - self._client = client - self._login = client._login - self._account = account - self._name = name - self._switch_property = switch_property - self._state = False - self._switch_function = switch_function - - async def async_added_to_hass(self): - """Store register state change callback.""" - try: - if not self.enabled: - return - except AttributeError: - pass - # Register event handler on bus - self._listener = self.hass.bus.async_listen( - f"{ALEXA_DOMAIN}_{hide_email(self._login.email)}"[0:32], self._handle_event - ) - - async def async_will_remove_from_hass(self): - """Prepare to remove entity.""" - # Register event handler on bus - self._listener() - - def _handle_event(self, event): - """Handle events. - - This will update PUSH_MEDIA_QUEUE_CHANGE events to see if the switch - should be updated. - """ - try: - if not self.enabled: - return - except AttributeError: - pass - if "queue_state" in event.data: - queue_state = event.data["queue_state"] - if queue_state["dopplerId"]["deviceSerialNumber"] == self._client.unique_id: - self._state = getattr(self._client, self._switch_property) - self.async_schedule_update_ha_state() - - @_catch_login_errors - async def _set_switch(self, state, **kwargs): - try: - if not self.enabled: - return - except AttributeError: - pass - success = await self._switch_function(state) - # if function returns success, make immediate state change - if success: - setattr(self._client, self._switch_property, state) - _LOGGER.debug( - "Switch set to %s based on %s", - getattr(self._client, self._switch_property), - state, - ) - self.async_schedule_update_ha_state() - elif self.should_poll: - # if we need to poll, refresh media_client - _LOGGER.debug( - "Requesting update of %s due to %s switch to %s", - self._client, - self._name, - state, - ) - await self._client.async_update() - - @property - def is_on(self): - """Return true if on.""" - return self.available and getattr(self._client, self._switch_property) - - async def async_turn_on(self, **kwargs): - """Turn on switch.""" - await self._set_switch(True, **kwargs) - - async def async_turn_off(self, **kwargs): - """Turn off switch.""" - await self._set_switch(False, **kwargs) - - @property - def available(self): - """Return the availabilty of the switch.""" - return getattr(self._client, self._switch_property) is not None - - @property - def unique_id(self): - """Return the unique ID.""" - return self._client.unique_id + "_" + self._name - - @property - def name(self): - """Return the name of the switch.""" - return "{} {} switch".format(self._client.name, self._name) - - @property - def device_class(self): - """Return the device_class of the switch.""" - return "switch" - - @property - def hidden(self): - """Return whether the switch should be hidden from the UI.""" - return not self.available - - @property - def should_poll(self): - """Return the polling state.""" - return not ( - self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._account]["websocket"] - ) - - @_catch_login_errors - async def async_update(self): - """Update state.""" - try: - if not self.enabled: - return - except AttributeError: - pass - try: - self.async_schedule_update_ha_state() - except NoEntitySpecifiedError: - pass # we ignore this due to a harmless startup race condition - - @property - def device_info(self): - """Return device_info for device registry.""" - return { - "identifiers": {(ALEXA_DOMAIN, self._client.unique_id)}, - "via_device": (ALEXA_DOMAIN, self._client.unique_id), - } - - @property - def icon(self): - """Return the icon of the switch.""" - return self._icon() - - def _icon(self, on=None, off=None): - return on if self.is_on else off - - -class DNDSwitch(AlexaMediaSwitch): - """Representation of a Alexa Media Do Not Disturb switch.""" - - def __init__(self, client, account): - """Initialize the Alexa Switch.""" - # Class info - super().__init__( - client, - "dnd_state", - client.alexa_api.set_dnd_state, - account, - "do not disturb", - ) - - @property - def icon(self): - """Return the icon of the switch.""" - return super()._icon("mdi:do-not-disturb", "mdi:do-not-disturb-off") - - def _handle_event(self, event): - """Handle events. - - This will update PUSH_EQUALIZER_STATE_CHANGE events to see if the DND switch - should be updated. - """ - try: - if not self.enabled: - return - except AttributeError: - pass - if "player_state" in event.data: - queue_state = event.data["player_state"] - if queue_state["dopplerId"]["deviceSerialNumber"] == self._client.unique_id: - self._state = getattr(self._client, self._switch_property) - self.async_schedule_update_ha_state() - - -class ShuffleSwitch(AlexaMediaSwitch): - """Representation of a Alexa Media Shuffle switch.""" - - def __init__(self, client, account): - """Initialize the Alexa Switch.""" - # Class info - super().__init__( - client, "shuffle", client.alexa_api.shuffle, account, "shuffle" - ) - - @property - def icon(self): - """Return the icon of the switch.""" - return super()._icon("mdi:shuffle", "mdi:shuffle-disabled") - - -class RepeatSwitch(AlexaMediaSwitch): - """Representation of a Alexa Media Repeat switch.""" - - def __init__(self, client, account): - """Initialize the Alexa Switch.""" - # Class info - super().__init__( - client, "repeat_state", client.alexa_api.repeat, account, "repeat" - ) - - @property - def icon(self): - """Return the icon of the switch.""" - return super()._icon("mdi:repeat", "mdi:repeat-off") diff --git a/custom_components/garbage_collection/.translations/cs.json b/custom_components/garbage_collection/.translations/cs.json deleted file mode 100644 index 92f5ada..0000000 --- a/custom_components/garbage_collection/.translations/cs.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "config": { - "title": "Garbage Collection", - "step": { - "user": { - "title": "Garbage Collection - Konfigurace", - "description": "Zadej jméno sensoru a nastav parametry. Více na https://github.com/bruxy70/Garbage-Collection", - "data": { - "name": "Friendly name", - "frequency": "Frekvence", - "icon_normal": "Ikona", - "icon_tomorrow": "Ikona svoz zítra", - "icon_today": "Ikona svoz dnes", - "verbose_state": "Verbose state (popis místo čísel)", - "verbose_format": "Verbose format (Pouýij `date` a `days` proměnné (ve složených závorkách))", - "date_format": "Formát data(see http://strftime.org/)" - } - }, - "annual_group": { - "title": "Garbage Collection - Další parametry", - "description": "Zadej datum svozu (např. 11/20 pro 20. listoped)", - "data": { - "date": "Datum (mm/dd)", - "entities": "Seznam entit (odděleno čárkou)" - } - }, - "detail": { - "title": "Garbage Collection - Dny svozu", - "description": "Vyber jeden nebo více dnů svozu. Více na: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Pondělí", - "collection_days_tue": "Úterý", - "collection_days_wed": "Středa", - "collection_days_thu": "Čtvrtek", - "collection_days_fri": "Pátek", - "collection_days_sat": "Sobota", - "collection_days_sun": "Neděle", - "force_week_order_numbers": "Číslo týdne v měsící místo čísla dne" - } - }, - "final": { - "title": "Garbage Collection - Další parametry", - "description": "Přidané a zakázané datumy jsou seznamy datumů ve formátu yyyy-mm-dd, oddělených čárkou. Více info na https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "První měsíc svozu", - "last_month": "Poslední měsíc scozu", - "period": "Perioda (svoz každých n týdnů/dnů): (1-52)", - "first_week": "První týden svozu (1-52)", - "first_date": "První datum", - "weekday_order_number_1": "Prvn den v měsíci", - "weekday_order_number_2": "Druhý den v měsíci", - "weekday_order_number_3": "Třetí den v měsíci", - "weekday_order_number_4": "Čtvrtý den v měsíci", - "weekday_order_number_5": "Pátý den v měsíci", - "week_order_number_1": "Prvn týden v měsíci", - "week_order_number_2": "Druhý týden v měsíci", - "week_order_number_3": "Třetí týden v měsíci", - "week_order_number_4": "Čtvrtý týden v měsíci", - "week_order_number_5": "Pátý týden v měsíci", - "include_dates": "Přidané datumy (volitelné)", - "exclude_dates": "Zakázané datumy (volitelné)", - "move_country_holidays": "Státní svátky - země (volitelné)", - "prov": "Státní svátky - provincie (volitelné)", - "state": "Státní svátky - stát (volitelné)", - "observed": "Státní svátky - observed (volitelné)" - - } - } - }, - "error": { - "name": "Jméno sensoru nemůže být prázdné.", - "days": "VYber jeden nebo více dní!", - "entities": "Entita neexistuje!", - "month_day": "Špatný formát data!", - "weekday_order_number": "Vyber jeden nebo více dní!", - "period": "Perioda musí být číslo mezi 1 a 52", - "first_week": "První týden musí být číslo mezi 1 a 52", - "date": "Špatný formát data!" - - }, - "abort": { - "single_instance_allowed": "Only a single configuration of Garbage Collection is allowed." - } - }, - "options": { - "step": { - "init": { - "title": "Garbage Collection - Konfigurace", - "description": "Zadej jméno sensoru a nastav parametry. Více na https://github.com/bruxy70/Garbage-Collection", - "data": { - "frequency": "Frekvence", - "icon_normal": "Ikona", - "icon_tomorrow": "Ikona svoz zítra", - "icon_today": "Ikona svoz dnes", - "verbose_state": "Verbose state (popis místo čísel)", - "verbose_format": "Verbose format (Pouýij `date` a `days` proměnné (ve složených závorkách))", - "date_format": "Formát data(see http://strftime.org/)" - } - }, - "annual_group": { - "title": "Garbage Collection - Další parametry", - "description": "Zadej datum svozu (např. 11/20 pro 20. listoped)", - "data": { - "date": "Datum (mm/dd)", - "entities": "Seznam entit (odděleno čárkou)" - } - }, - "detail": { - "title": "Garbage Collection - Dny svozu", - "description": "Vyber jeden nebo více dnů svozu. Více na: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Pondělí", - "collection_days_tue": "Úterý", - "collection_days_wed": "Středa", - "collection_days_thu": "Čtvrtek", - "collection_days_fri": "Pátek", - "collection_days_sat": "Sobota", - "collection_days_sun": "Neděle", - "force_week_order_numbers": "Číslo týdne v měsící místo čísla dne" - - } - }, - "final": { - "title": "Garbage Collection - Další parametry", - "description": "Přidané a zakázané datumy jsou seznamy datumů ve formátu yyyy-mm-dd, oddělených čárkou. Více info na https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "První měsíc svozu", - "last_month": "Poslední měsíc scozu", - "period": "Perioda (svoz každých n týdnů-dnů): (1-52)", - "first_week": "První týden svozu (1-52)", - "first_date": "První datum", - "weekday_order_number_1": "Prvn den v měsíci", - "weekday_order_number_2": "Druhý den v měsíci", - "weekday_order_number_3": "Třetí den v měsíci", - "weekday_order_number_4": "Čtvrtý den v měsíci", - "weekday_order_number_5": "Pátý den v měsíci", - "week_order_number_1": "Prvn týden v měsíci", - "week_order_number_2": "Druhý týden v měsíci", - "week_order_number_3": "Třetí týden v měsíci", - "week_order_number_4": "Čtvrtý týden v měsíci", - "week_order_number_5": "Pátý týden v měsíci", - "include_dates": "Přidané datumy (volitelné)", - "exclude_dates": "Zakázané datumy (volitelné)", - "move_country_holidays": "Státní svátky - země (volitelné)", - "prov": "Státní svátky - provincie (volitelné)", - "state": "Státní svátky - stát (volitelné)", - "observed": "Státní svátky - observed (volitelné)" - } - } - }, - "error": { - "date": "Špatný formát data!", - "entities": "Entita neexistuje!" - } - } - -} \ No newline at end of file diff --git a/custom_components/garbage_collection/.translations/en.json b/custom_components/garbage_collection/.translations/en.json deleted file mode 100644 index 0ae5f67..0000000 --- a/custom_components/garbage_collection/.translations/en.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "config": { - "title": "Garbage Collection", - "step": { - "user": { - "title": "Garbage Collection - Collection frequency", - "description": "Enter the sensor name and configure sensor parameters. More info on https://github.com/bruxy70/Garbage-Collection", - "data": { - "name": "Friendly name", - "frequency": "Frequency", - "icon_normal": "Icon", - "icon_tomorrow": "Icon collectio tomorrow", - "icon_today": "Icon collection today", - "verbose_state": "Verbose state (text, instead of number)", - "verbose_format": "Verbose format (using `date` and `days` variables (in squary brackets))", - "date_format": "Date format (see http://strftime.org/)" - } - }, - "annual_group": { - "title": "Garbage Collection - Additional parameters", - "description": "Enter the collection day date (e.g. 11/20 for November 20)", - "data": { - "date": "Date (mm/dd)", - "entities": "List of entities (comma separated)" - } - }, - "detail": { - "title": "Garbage Collection - Collection days", - "description": "Select one or more collection days. More details here: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Monday", - "collection_days_tue": "Tuesday", - "collection_days_wed": "Wednesday", - "collection_days_thu": "Thursday", - "collection_days_fri": "Friday", - "collection_days_sat": "Saturday", - "collection_days_sun": "Sunday", - "force_week_order_numbers": "Order of week in a month instead of order of weekday" - } - }, - "final": { - "title": "Garbage Collection - Additional parameters", - "description": "Include and exclude dates are comma separated lists of dates, in format yyyy-mm-dd. More info on https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "First collection month", - "last_month": "Last collection month", - "period": "Collection every n weeks/days: (1-52)", - "first_week": "First collection week (1-52)", - "first_date": "First date", - "weekday_order_number_1": "1st weekday of month", - "weekday_order_number_2": "2nd weekday of month", - "weekday_order_number_3": "3rd weekday of month", - "weekday_order_number_4": "4th weekday of month", - "weekday_order_number_5": "5th weekday of month", - "week_order_number_1": "1st week of month", - "week_order_number_2": "2nd week of month", - "week_order_number_3": "3rd week of month", - "week_order_number_4": "4th week of month", - "week_order_number_5": "5th week of month", - "include_dates": "Include dates (optional)", - "exclude_dates": "Exclude dates (optional)", - "move_country_holidays": "Move holidays to next day (optional)", - "prov": "Country holidays - prov (optional)", - "state": "Country holidays - state (optional)", - "observed": "Country holidays - observed (optional)" - } - } - }, - "error": { - "name": "The sensor name cannot be empty. Please enter the sensor name.", - "days": "Select 1 or more days!", - "entities": "Entity does not exist!", - "month_day": "Invalid date format!", - "weekday_order_number": "Select 1 or more days", - "period": "Period must be a number between 1 and 52", - "first_week": "Firsr week must be a number between 1 and 52", - "date": "Invalid date format!" - - }, - "abort": { - "single_instance_allowed": "Only a single configuration of Garbage Collection is allowed." - } - }, - "options": { - "step": { - "init": { - "title": "Garbage Collection - Collection frequency", - "description": "Change sensor parameters. More info on https://github.com/bruxy70/Garbage-Collection", - "data": { - "frequency": "Frequency", - "icon_normal": "Icon", - "icon_tomorrow": "Icon collectio tomorrow", - "icon_today": "Icon collection today", - "verbose_state": "Verbose state (text, instead of number)", - "verbose_format": "Verbose format (using `date` and `days` variables (in squary brackets))", - "date_format": "Date format (see http://strftime.org/)" - } - }, - "annual_group": { - "title": "Garbage Collection - Additional parameters", - "description": "Enter the collection day date (e.g. 11/20 for November 20)", - "data": { - "date": "Date (mm/dd)", - "entities": "List of entities (comma separated)" - } - }, - "detail": { - "title": "Garbage Collection - Collection days", - "description": "Select one or more collection days. More details here: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Monday", - "collection_days_tue": "Tuesday", - "collection_days_wed": "Wednesday", - "collection_days_thu": "Thursday", - "collection_days_fri": "Friday", - "collection_days_sat": "Saturday", - "collection_days_sun": "Sunday", - "force_week_order_numbers": "Order of week in a month instead of order of weekday" - } - }, - "final": { - "title": "Garbage Collection - Additional parameters", - "description": "Include and exclude dates are comma separated lists of dates, in format yyyy-mm-dd. More info on https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "First collection month", - "last_month": "Last collection month", - "period": "Collection every n weeks/days: (1-52)", - "first_week": "First collection week (1-52)", - "first_date": "First date", - "weekday_order_number_1": "1st weekday of month", - "weekday_order_number_2": "2nd weekday of month", - "weekday_order_number_3": "3rd weekday of month", - "weekday_order_number_4": "4th weekday of month", - "weekday_order_number_5": "5th weekday of month", - "week_order_number_1": "1st week of month", - "week_order_number_2": "2nd week of month", - "week_order_number_3": "3rd week of month", - "week_order_number_4": "4th week of month", - "week_order_number_5": "5th week of month", - "include_dates": "Include dates (optional)", - "exclude_dates": "Exclude dates (optional)", - "move_country_holidays": "Move holidays to next day (optional)", - "prov": "Country holidays - prov (optional)", - "state": "Country holidays - state (optional)", - "observed": "Country holidays - observed (optional)" - } - } - }, - "error": { - "date": "Invalid date format!", - "entities": "Entity does not exist!" - } - } - -} \ No newline at end of file diff --git a/custom_components/garbage_collection/.translations/es.json b/custom_components/garbage_collection/.translations/es.json deleted file mode 100644 index 52ab53a..0000000 --- a/custom_components/garbage_collection/.translations/es.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "config": { - "title": "Recolección de basura", - "step": { - "user": { - "title": "Recolección de basura - Frecuencia de recogida", - "description": "Ingrese el nombre del sensor y configure los parámetros del sensor. Más información en https://github.com/bruxy70/Garbage-Collection", - "data": { - "name": "Nombre amigable", - "frequency": "Frequencia", - "icon_normal": "Icono", - "icon_tomorrow": "Icono de recoleccion para mañana", - "icon_today": "Icono de recoleccion para hoy", - "verbose_state": "Estado detallado (texto, en lugar de número)", - "verbose_format": "Formato detallado (usando las variables `fecha` y` dias` (entre corchetes))", - "date_format": "Formato de fecha(ver en http://strftime.org/)" - } - }, - "annual_group": { - "title": "Recolección de basura - Parámetros adicionales", - "description": "Ingrese la fecha del día de recolección (por ejemplo, 20/11 para el 20 de noviembre)", - "data": { - "date": "Fecha (mm/dd)", - "entities": "Lista de entidades (separadas por comas)" - } - }, - "detail": { - "title": "Recolección de basura - Días de recogida", - "description": "Seleccione uno o más días de recolección. Más detalles aquí: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Lunes", - "collection_days_tue": "Martes", - "collection_days_wed": "Miercoles", - "collection_days_thu": "Jueves", - "collection_days_fri": "Viernes", - "collection_days_sat": "Sabado", - "collection_days_sun": "Domingo", - "force_week_order_numbers": "Orden de la semana en un mes en lugar del orden del día de la semana" - } - }, - "final": { - "title": "Recolección de basura - Parámetros adicionales", - "description": "Las fechas de inclusión y exclusión son listas de fechas separadas por comas, en formato yyyy-mm-dd. Más información en https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "Primer mes de recogida", - "last_month": "Último mes de recogida", - "period": "Recolección cada n semanas / días: (1-52)", - "first_week": "Primera semana de recolección (1-52)", - "first_date": "Primera fecha", - "weekday_order_number_1": "Primer día de la semana del mes", - "weekday_order_number_2": "Segundo día de la semana del mes", - "weekday_order_number_3": "Tercer día de la semana del mes", - "weekday_order_number_4": "Cuarto día de la semana del mes", - "weekday_order_number_5": "Quinto día de la semana del mes", - "week_order_number_1": "1ra semana del mes", - "week_order_number_2": "2da semana del mes", - "week_order_number_3": "3ra semana del mes", - "week_order_number_4": "4ta semana del mes", - "week_order_number_5": "5ta semana del mes", - "include_dates": "Incluir fechas (opcional)", - "exclude_dates": "Excluir fechas (opcional)", - "move_country_holidays": "Mover vacaciones al día siguiente (opcional)", - "prov": "Días festivos - prov (opcional)", - "state": "Días festivos - estado (opcional)", - "observed": "Días festivos - observado (opcional)" - } - } - }, - "error": { - "name": "El nombre del sensor no puede estar vacío. Por favor, introduzca el nombre del sensor.", - "days": "Seleccione 1 o más días!", - "entities": "La entidad no existe!", - "month_day": "Formato de fecha inválido!", - "weekday_order_number": "Seleccione 1 o más días", - "period": "El período debe ser un número entre 1 y 52", - "first_week": "La primera semana debe ser un número entre 1 y 52", - "date": "Formato de fecha inválido!" - - }, - "abort": { - "single_instance_allowed": "Solo se permite una única configuración de Recolección de basura." - } - }, - "options": { - "step": { - "init": { - "title": "Recolección de basura - Frecuencia de recogida", - "description": "Cambiar los parámetros del sensor. Más información en https://github.com/bruxy70/Garbage-Collection", - "data": { - "frequency": "Frequencia", - "icon_normal": "Icono", - "icon_tomorrow": "Icono de recoleccion para mañana", - "icon_today": "Icono de recoleccion para hoy", - "verbose_state": "Estado detallado (texto, en lugar de número)", - "verbose_format": "Formato detallado (usando las variables `fecha` y` dias` (entre corchetes))", - "date_format": "Formato de fecha(ver en http://strftime.org/)" - } - }, - "annual_group": { - "title": "Recolección de basura - Parámetros adicionales", - "description": "Ingrese la fecha del día de recolección (por ejemplo, 20/11 para el 20 de noviembre)", - "data": { - "date": "Fecha (mm/dd)", - "entities": "Lista de entidades (separadas por comas)" - } - }, - "detail": { - "title": "Recolección de basura - Días de recogida", - "description": "Seleccione uno o más días de recolección. Más detalles aquí: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Lunes", - "collection_days_tue": "Martes", - "collection_days_wed": "Miercoles", - "collection_days_thu": "Jueves", - "collection_days_fri": "Viernes", - "collection_days_sat": "Sabado", - "collection_days_sun": "Domingo", - "force_week_order_numbers": "Orden de la semana en un mes en lugar del orden del día de la semana" - } - }, - "final": { - "title": "Recolección de basura - Parámetros adicionales", - "description": "Las fechas de inclusión y exclusión son listas de fechas separadas por comas, en formato yyyy-mm-dd. Más información en https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "Primer mes de recogida", - "last_month": "Último mes de recogida", - "period": "Recolección cada n semanas / días: (1-52)", - "first_week": "Primera semana de recolección (1-52)", - "first_date": "Primera fecha", - "weekday_order_number_1": "Primer día de la semana del mes", - "weekday_order_number_2": "Segundo día de la semana del mes", - "weekday_order_number_3": "Tercer día de la semana del mes", - "weekday_order_number_4": "Cuarto día de la semana del mes", - "weekday_order_number_5": "Quinto día de la semana del mes", - "week_order_number_1": "1ra semana del mes", - "week_order_number_2": "2da semana del mes", - "week_order_number_3": "3ra semana del mes", - "week_order_number_4": "4ta semana del mes", - "week_order_number_5": "5ta semana del mes", - "include_dates": "Incluir fechas (opcional)", - "exclude_dates": "Excluir fechas (opcional)", - "move_country_holidays": "Mover vacaciones al día siguiente (opcional)", - "prov": "Días festivos - prov (opcional)", - "state": "Días festivos - estado (opcional)", - "observed": "Días festivos - observado (opcional)" - } - } - }, - "error": { - "date": "Formato de fecha inválido!", - "entities": "La entidad no existe!" - } - } - -} diff --git a/custom_components/garbage_collection/.translations/fr.json b/custom_components/garbage_collection/.translations/fr.json deleted file mode 100644 index 987dd68..0000000 --- a/custom_components/garbage_collection/.translations/fr.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "config": { - "title": "Garbage Collection", - "step": { - "user": { - "title": "Garbage Collection - Fréquence de la collecte", - "description": "Définir le nom du capteur et configurer les paramètres du capteur. Plus d'info sur https://github.com/bruxy70/Garbage-Collection", - "data": { - "name": "Friendly name", - "frequency": "Fréquence", - "icon_normal": "Icone", - "icon_tomorrow": "Icone pour collecte à jour J+1", - "icon_today": "Icone pour collecte au jour J", - "verbose_state": "Etat verbeux (texte, au lieu du chiffre)", - "verbose_format": "Format état verbeux (utilisation des variables `date` et `days` (entre caractères accolade ))", - "date_format": "Format de date (voir http://strftime.org/)" - } - }, - "annual_group": { - "title": "Garbage Collection - Paramètres additionnels", - "description": "Entrer la date du jour de collecte (Ex: 11/20 pour le 20 Novembre)", - "data": { - "date": "Date (mm/dd)", - "entities": "Liste des entités (séparées par une virgule)" - } - }, - "detail": { - "title": "Garbage Collection - Jours de collecte", - "description": "Choisir un ou plusieurs jours de collecte. Plus de détail ici: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Lundi", - "collection_days_tue": "Mardi", - "collection_days_wed": "Mercredi", - "collection_days_thu": "Jeudi", - "collection_days_fri": "Vendredi", - "collection_days_sat": "Samedi", - "collection_days_sun": "Dimance", - "force_week_order_numbers": "Nième occurrence du jour de la semaine dans un mois, au lieu du jour de la semaine dans la Nième semaine de chaque mois. Plus d'info sur https://github.com/bruxy70/Garbage-Collection#parameters-for-monthly-collection" - } - }, - "final": { - "title": "Garbage Collection - Paramètres additionnels", - "description": "Liste de dates, séparées par des virgules, d'incluston et d'exclusion. Les dates sont au format yyyy-mm-dd. Plus d'info sur https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "Premier mois de la collecte", - "last_month": "Dernier mois de la collecte", - "period": "Collecte toutes les n semaines/jours: (1-52)", - "first_week": "Première semaine de la collecte (1-52)", - "first_date": "Première date", - "weekday_order_number_1": "1er jour de semaine du mois", - "weekday_order_number_2": "2ème jour de semaine du mois", - "weekday_order_number_3": "3ème jour de semaine du mois", - "weekday_order_number_4": "4ème jour de semaine du mois", - "weekday_order_number_5": "5th jour de semaine du mois", - "week_order_number_1": "1ère semaine du mois", - "week_order_number_2": "2ème semaine du mois", - "week_order_number_3": "3ème semaine du mois", - "week_order_number_4": "4ème semaine du mois", - "week_order_number_5": "5ème semaine du mois", - "include_dates": "Inclusion de dates (optionel)", - "exclude_dates": "Exclusion de dates (optionel)", - "move_country_holidays": "Move holidays to next day (optional)" - } - } - }, - "error": { - "name": "Le nom du capteur ne peut pas être vide. Merci d'entrer un nom pour le capteur.", - "days": "Choisir un ou plusieurs jours!", - "entities": "L'entité n'existe pas!", - "month_day": "Format de date invalide!", - "weekday_order_number": "Choisir un ou plusieurs jours", - "period": "Period doit être un nombre entre 1 et 52", - "first_week": "La première semaine doit être un nombre entre 1 et 52", - "date": "Format de date invalide!" - - }, - "abort": { - "single_instance_allowed": "Une seule configuration de Garbage Collection est autorisée" - } - }, - "options": { - "step": { - "init": { - "title": "Garbage Collection - Fréquence de la collecte", - "description": "Modifier les paramètres des capteur. Plus d'infos sur https://github.com/bruxy70/Garbage-Collection", - "data": { - "frequency": "Fréquence", - "icon_normal": "Icone", - "icon_tomorrow": "Icone pour collecte à jour J+1", - "icon_today": "Icone pour collecte au jour J", - "verbose_state": "Etat verbeux (texte, au lieu du chiffre)", - "verbose_format": "Format état verbeux (utilisation des variables `date` et `days` (entre caractères accolade ))", - "date_format": "Format de date (voir http://strftime.org/)" - } - }, - "annual_group": { - "title": "Garbage Collection - Paramètres additionnels", - "description": "Entrer la date du jour de collecte (Ex: 11/20 pour le 20 Novembre)", - "data": { - "date": "Date (mm/dd)", - "entities": "Liste des entités (séparées par une virgule)" - } - }, - "detail": { - "title": "Garbage Collection - Collection days", - "description": "Choisir un ou plusieurs jours de collecte. Plus de détail ici: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Lundi", - "collection_days_tue": "Mardi", - "collection_days_wed": "Mercredi", - "collection_days_thu": "Jeudi", - "collection_days_fri": "Vendredi", - "collection_days_sat": "Samedi", - "collection_days_sun": "Dimance", - "force_week_order_numbers": "Nième occurrence du jour de la semaine dans un mois, au lieu du jour de la semaine dans la Nième semaine de chaque mois. Plus d'info sur https://github.com/bruxy70/Garbage-Collection#parameters-for-monthly-collection" - } - }, - "final": { - "title": "Garbage Collection - Paramètres additionnels", - "description": "Liste de dates, séparées par des virgules, d'incluston et d'exclusion. Les dates sont au format yyyy-mm-dd. Plus d'info sur https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "Premier mois de la collecte", - "last_month": "Dernier mois de la collecte", - "period": "Collecte toutes les n semaines/jours: (1-52)", - "first_week": "Première semaine de la collecte (1-52)", - "first_date": "Première date", - "weekday_order_number_1": "1er jour de semaine du mois", - "weekday_order_number_2": "2ème jour de semaine du mois", - "weekday_order_number_3": "3ème jour de semaine du mois", - "weekday_order_number_4": "4ème jour de semaine du mois", - "weekday_order_number_5": "5th jour de semaine du mois", - "week_order_number_1": "1ère semaine du mois", - "week_order_number_2": "2ème semaine du mois", - "week_order_number_3": "3ème semaine du mois", - "week_order_number_4": "4ème semaine du mois", - "week_order_number_5": "5ème semaine du mois", - "include_dates": "Inclusion de dates (optionel)", - "exclude_dates": "Exclusion de dates (optionel)", - "move_country_holidays": "Move holidays to next day (optional)" - } - } - }, - "error": { - "date": "Format de date invalide!", - "entities": "L'entité n'existe pas!" - } - } - -} diff --git a/custom_components/garbage_collection/.translations/it.json b/custom_components/garbage_collection/.translations/it.json deleted file mode 100644 index 511c2fb..0000000 --- a/custom_components/garbage_collection/.translations/it.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "config": { - "title": "Raccolta Differenziata", - "step": { - "user": { - "title": "Raccolta Differenziata - Frequenza di raccolta", - "description": "Immetti il nome del sensore e configura i suoi parametri. Maggiori informazioni su https://github.com/bruxy70/Garbage-Collection", - "data": { - "name": "Nome personalizzato", - "frequency": "Frequenza", - "icon_normal": "Icona", - "icon_tomorrow": "Icona raccolta domani", - "icon_today": "Icona raccolta oggi", - "verbose_state": "Stato Verbale (testo, al posto di numeri)", - "verbose_format": "Formato Verbale (usare le variabili `date` e `days` (in parentesi graffe))", - "date_format": "Formato Data (vedere http://strftime.org/)" - } - }, - "annual_group": { - "title": "Raccolta Differenziata - Parametri aggiuntivi", - "description": "Inserire la data del giorno della raccolta (es. 11/20 per Novembre 20)", - "data": { - "date": "Data (mm/dd)", - "entities": "Lista di entità (separate da virgole)" - } - }, - "detail": { - "title": "Raccolta Differenziata - Giorni di raccolta", - "description": "Seleziona uno o più giorni di raccolta. Maggiori dettagli qui: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Lunedì", - "collection_days_tue": "Martedì", - "collection_days_wed": "Mercoledì", - "collection_days_thu": "Giovedì", - "collection_days_fri": "Venerdì", - "collection_days_sat": "Sabato", - "collection_days_sun": "Domenica", - "force_week_order_numbers": "Ordina per settimana in un mese anziché per giorno della settimana" - } - }, - "final": { - "title": "Raccolta Differenziata - Parametri aggiuntivi", - "description": "'Includi ed escludi date' sono liste di date separate da virgole, nel formato yyyy-mm-dd. Maggiori informazioni su https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "Prima raccolta del mese", - "last_month": "Ultima raccolta del mese", - "period": "Raccolta ogni n settimane/giorno: (1-52)", - "first_week": "Prima raccolta della settimana (1-52)", - "first_date": "Prima date", - "weekday_order_number_1": "1° giorno della settimana del mese", - "weekday_order_number_2": "2° giorno della settimana del mese", - "weekday_order_number_3": "3° giorno della settimana del mese", - "weekday_order_number_4": "4° giorno della settimana del mese", - "weekday_order_number_5": "5° giorno della settimana del mese", - "week_order_number_1": "1° settimana del mese", - "week_order_number_2": "2° settimana del mese", - "week_order_number_3": "3° settimana del mese", - "week_order_number_4": "4° settimana del mese", - "week_order_number_5": "5° settimana del mese", - "include_dates": "Includi date (opzionale)", - "exclude_dates": "Escludi date (opzionale)", - "move_country_holidays": "Move holidays to next day (optional)" - } - } - }, - "error": { - "name": "Il nome del sensore non può essere vuoto. Immettere il nome del sensore.", - "days": "Seleziona uno o più giorni!", - "entities": "L'entità non esiste!", - "month_day": "Formato data non valido!", - "weekday_order_number": "Seleziona uno o più giorni", - "period": "Il periodo deve essere un numero compreso tra 1 e 52", - "first_week": "La prima settimana deve essere un numero tra 1 e 52", - "date": "Formato data non valido!" - - }, - "abort": { - "single_instance_allowed": "E' consentita solo una singola configurazione di Raccolta Differenziata." - } - }, - "options": { - "step": { - "init": { - "title": "Raccolta Differenziata - Frequenza di raccolta", - "description": "Cambia i parametri del sensore. Maggiori informazioni su https://github.com/bruxy70/Garbage-Collection", - "data": { - "frequency": "Frequenza", - "icon_normal": "Icona", - "icon_tomorrow": "Icona raccolta domani", - "icon_today": "Icona raccolta oggi", - "verbose_state": "Stato Verbale (testo, al posto di numeri)", - "verbose_format": "Formato Verbale (usare le variabili `date` e `days` (in parentesi graffe))", - "date_format": "Formato Data (vedere http://strftime.org/)" - } - }, - "annual_group": { - "title": "Raccolta Differenziata - Parametri aggiuntivi", - "description": "Inserire la data del giorno della raccolta (es. 11/20 per Novembre 20)", - "data": { - "date": "Data (mm/dd)", - "entities": "Lista di entità (separate da virgole)" - } - }, - "detail": { - "title": "Raccolta Differenziata - Giorni di raccolta", - "description": "Seleziona uno o più giorni di raccolta. Maggiori dettagli qui: https://github.com/bruxy70/Garbage-Collection", - "data": { - "collection_days_mon": "Lunedì", - "collection_days_tue": "Martedì", - "collection_days_wed": "Mercoledì", - "collection_days_thu": "Giovedì", - "collection_days_fri": "Venerdì", - "collection_days_sat": "Sabato", - "collection_days_sun": "Domenica", - "force_week_order_numbers": "Ordina per settimana in un mese anziché per giorno della settimana" - } - }, - "final": { - "title": "Raccolta Differenziata - Parametri aggiuntivi", - "description": "'Includi ed escludi date' sono liste di date separate da virgole, nel formato yyyy-mm-dd. Maggiori informazioni su https://github.com/bruxy70/Garbage-Collection", - "data": { - "first_month": "Prima raccolta del mese", - "last_month": "Ultima raccolta del mese", - "period": "Raccolta ogni n settimane/giorno: (1-52)", - "first_week": "Prima raccolta della settimana (1-52)", - "first_date": "Prima date", - "weekday_order_number_1": "1° giorno della settimana del mese", - "weekday_order_number_2": "2° giorno della settimana del mese", - "weekday_order_number_3": "3° giorno della settimana del mese", - "weekday_order_number_4": "4° giorno della settimana del mese", - "weekday_order_number_5": "5° giorno della settimana del mese", - "week_order_number_1": "1° settimana del mese", - "week_order_number_2": "2° settimana del mese", - "week_order_number_3": "3° settimana del mese", - "week_order_number_4": "4° settimana del mese", - "week_order_number_5": "5° settimana del mese", - "include_dates": "Includi date (opzionale)", - "exclude_dates": "Escludi date (opzionale)", - "move_country_holidays": "Move holidays to next day (optional)" - } - } - }, - "error": { - "date": "Formato data non valido!", - "entities": "L'entità non esiste!" - } - } - -} diff --git a/custom_components/garbage_collection/.translations/sensor.cs.json b/custom_components/garbage_collection/.translations/sensor.cs.json deleted file mode 100644 index a243c72..0000000 --- a/custom_components/garbage_collection/.translations/sensor.cs.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "today": "Dnes", - "tomorrow": "Zítra" - } -} \ No newline at end of file diff --git a/custom_components/garbage_collection/.translations/sensor.de.json b/custom_components/garbage_collection/.translations/sensor.de.json deleted file mode 100644 index 39f748b..0000000 --- a/custom_components/garbage_collection/.translations/sensor.de.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "today": "Heute", - "tomorrow": "Morgen" - } -} diff --git a/custom_components/garbage_collection/.translations/sensor.en.json b/custom_components/garbage_collection/.translations/sensor.en.json deleted file mode 100644 index ad074f4..0000000 --- a/custom_components/garbage_collection/.translations/sensor.en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "today": "Today", - "tomorrow": "Tomorrow" - } -} \ No newline at end of file diff --git a/custom_components/garbage_collection/.translations/sensor.es.json b/custom_components/garbage_collection/.translations/sensor.es.json deleted file mode 100644 index d365f7a..0000000 --- a/custom_components/garbage_collection/.translations/sensor.es.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "today": "Hoy", - "tomorrow": "Mañana" - } -} diff --git a/custom_components/garbage_collection/.translations/sensor.fr.json b/custom_components/garbage_collection/.translations/sensor.fr.json deleted file mode 100644 index da22ae4..0000000 --- a/custom_components/garbage_collection/.translations/sensor.fr.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "today": "Aujourd'hui", - "tomorrow": "Demain" - } -} diff --git a/custom_components/garbage_collection/.translations/sensor.it.json b/custom_components/garbage_collection/.translations/sensor.it.json deleted file mode 100644 index ce8ff71..0000000 --- a/custom_components/garbage_collection/.translations/sensor.it.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "today": "Oggi", - "tomorrow": "Domani" - } -} diff --git a/custom_components/garbage_collection/.translations/sensor.nl.json b/custom_components/garbage_collection/.translations/sensor.nl.json deleted file mode 100644 index 60b5a26..0000000 --- a/custom_components/garbage_collection/.translations/sensor.nl.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "today": "Vandaag", - "tomorrow": "Morgen" - } -} diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py deleted file mode 100644 index 8a95b6d..0000000 --- a/custom_components/garbage_collection/__init__.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Component to integrate with garbage_colection. -""" -import os -from datetime import timedelta -import logging -from homeassistant import config_entries -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util import Throttle -from .sensor import GarbageCollection - -from integrationhelper.const import CC_STARTUP_VERSION - -from homeassistant.const import CONF_NAME - -from .const import ( - CONF_SENSORS, - CONF_ENABLED, - CONF_FREQUENCY, - DEFAULT_NAME, - DOMAIN_DATA, - DOMAIN, - ISSUE_URL, - PLATFORM, - VERSION, - CONFIG_SCHEMA, -) - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass, config): - """Set up this component using YAML.""" - if config.get(DOMAIN) is None: - # We get here if the integration is set up using config flow - return True - - # Print startup message - _LOGGER.info( - CC_STARTUP_VERSION.format(name=DOMAIN, version=VERSION, issue_link=ISSUE_URL) - ) - platform_config = config[DOMAIN].get(CONF_SENSORS, {}) - - # If platform is not enabled, skip. - if not platform_config: - return False - - for entry in platform_config: - # If entry is not enabled, skip. - # if not entry[CONF_ENABLED]: - # continue - hass.async_create_task( - discovery.async_load_platform(hass, PLATFORM, DOMAIN, entry, config) - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) - ) - return True - - -async def async_setup_entry(hass, config_entry): - """Set up this integration using UI.""" - if config_entry.source == config_entries.SOURCE_IMPORT: - # We get here if the integration is set up using YAML - hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) - return False - # Print startup message - _LOGGER.info( - CC_STARTUP_VERSION.format(name=DOMAIN, version=VERSION, issue_link=ISSUE_URL) - ) - config_entry.options = config_entry.data - config_entry.add_update_listener(update_listener) - # Add sensor - hass.async_add_job( - hass.config_entries.async_forward_entry_setup(config_entry, PLATFORM) - ) - return True - - -async def async_remove_entry(hass, config_entry): - """Handle removal of an entry.""" - try: - await hass.config_entries.async_forward_entry_unload(config_entry, PLATFORM) - _LOGGER.info( - "Successfully removed sensor from the garbage_collection integration" - ) - except ValueError: - pass - - -async def update_listener(hass, entry): - """Update listener.""" - entry.data = entry.options - await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) - hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, PLATFORM)) diff --git a/custom_components/garbage_collection/__pycache__/__init__.cpython-37.pyc b/custom_components/garbage_collection/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index e667d05..0000000 Binary files a/custom_components/garbage_collection/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/custom_components/garbage_collection/__pycache__/config_flow.cpython-37.pyc b/custom_components/garbage_collection/__pycache__/config_flow.cpython-37.pyc deleted file mode 100644 index cba8f1e..0000000 Binary files a/custom_components/garbage_collection/__pycache__/config_flow.cpython-37.pyc and /dev/null differ diff --git a/custom_components/garbage_collection/__pycache__/const.cpython-37.pyc b/custom_components/garbage_collection/__pycache__/const.cpython-37.pyc deleted file mode 100644 index d0c16cf..0000000 Binary files a/custom_components/garbage_collection/__pycache__/const.cpython-37.pyc and /dev/null differ diff --git a/custom_components/garbage_collection/__pycache__/sensor.cpython-37.pyc b/custom_components/garbage_collection/__pycache__/sensor.cpython-37.pyc deleted file mode 100644 index 57e3d51..0000000 Binary files a/custom_components/garbage_collection/__pycache__/sensor.cpython-37.pyc and /dev/null differ diff --git a/custom_components/garbage_collection/config_flow.py b/custom_components/garbage_collection/config_flow.py deleted file mode 100644 index b431c75..0000000 --- a/custom_components/garbage_collection/config_flow.py +++ /dev/null @@ -1,866 +0,0 @@ -"""Adds config flow for GarbageCollection.""" -from collections import OrderedDict -import logging -from homeassistant.core import callback -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant import config_entries -from datetime import datetime -import uuid - -from .const import ( - DOMAIN, - FREQUENCY_OPTIONS, - MONTH_OPTIONS, - WEEKLY_FREQUENCY, - WEEKLY_FREQUENCY_X, - DAILY_FREQUENCY, - MONTHLY_FREQUENCY, - ANNUAL_FREQUENCY, - GROUP_FREQUENCY, - COUNTRY_CODES, - DEFAULT_FIRST_MONTH, - DEFAULT_LAST_MONTH, - DEFAULT_FREQUENCY, - DEFAULT_ICON_NORMAL, - DEFAULT_ICON_TOMORROW, - DEFAULT_ICON_TODAY, - DEFAULT_VERBOSE_STATE, - DEFAULT_VERBOSE_FORMAT, - DEFAULT_DATE_FORMAT, - CONF_SENSOR, - CONF_ENABLED, - CONF_FREQUENCY, - CONF_ICON_NORMAL, - CONF_ICON_TODAY, - CONF_ICON_TOMORROW, - CONF_VERBOSE_STATE, - CONF_VERBOSE_FORMAT, - CONF_DATE_FORMAT, - CONF_FIRST_MONTH, - CONF_LAST_MONTH, - CONF_COLLECTION_DAYS, - CONF_FORCE_WEEK_NUMBERS, - CONF_WEEKDAY_ORDER_NUMBER, - CONF_WEEK_ORDER_NUMBER, - CONF_DATE, - CONF_EXCLUDE_DATES, - CONF_INCLUDE_DATES, - CONF_MOVE_COUNTRY_HOLIDAYS, - CONF_PROV, - CONF_STATE, - CONF_OBSERVED, - CONF_PERIOD, - CONF_FIRST_WEEK, - CONF_FIRST_DATE, - CONF_SENSORS, - ATTR_NEXT_DATE, -) - -from homeassistant.const import CONF_NAME, WEEKDAYS, CONF_ENTITIES - -_LOGGER = logging.getLogger(__name__) - - -def clean_optional(dict, key): - """Remove optional keys before update""" - if key in dict: - del dict[key] - - -@config_entries.HANDLERS.register(DOMAIN) -class GarbageCollectionFlowHandler(config_entries.ConfigFlow): - """Config flow for garbage_collection.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - - def __init__(self): - """Initialize.""" - self._errors = {} - self._data = {} - self._data["unique_id"] = str(uuid.uuid4()) - - async def async_step_user( - self, user_input={} - ): # pylint: disable=dangerous-default-value - """ - - C O N F I G U R A T I O N S T E P 1 - - """ - self._errors = {} - if user_input is not None: - if user_input[CONF_NAME] != "": - # Remember Frequency - self._data.update(user_input) - # Call next step - if ( - user_input[CONF_FREQUENCY] in ANNUAL_FREQUENCY - or user_input[CONF_FREQUENCY] in GROUP_FREQUENCY - ): - # Annual and group schedule is different (does not have days) - return await self.async_step_annual_group() - elif user_input[CONF_FREQUENCY] in DAILY_FREQUENCY: - return await self.async_step_final() - else: - return await self.async_step_detail() - else: - self._errors["base"] = "name" - return await self._show_user_form(user_input) - return await self._show_user_form(user_input) - - async def _show_user_form(self, user_input): - """Configuration STEP 1 - SHOW FORM""" - # Defaults - name = "" - frequency = DEFAULT_FREQUENCY - icon_normal = DEFAULT_ICON_NORMAL - icon_tomorrow = DEFAULT_ICON_TOMORROW - icon_today = DEFAULT_ICON_TODAY - verbose_state = DEFAULT_VERBOSE_STATE - verbose_format = DEFAULT_VERBOSE_FORMAT - date_format = DEFAULT_DATE_FORMAT - if user_input is not None: - if CONF_NAME in user_input: - name = user_input[CONF_NAME] - if CONF_FREQUENCY in user_input: - frequency = user_input[CONF_FREQUENCY] - if CONF_ICON_NORMAL in user_input: - icon_normal = user_input[CONF_ICON_NORMAL] - if CONF_ICON_TOMORROW in user_input: - icon_tomorrow = user_input[CONF_ICON_TOMORROW] - if CONF_ICON_TODAY in user_input: - icon_today = user_input[CONF_ICON_TODAY] - if CONF_VERBOSE_STATE in user_input: - verbose_state = user_input[CONF_VERBOSE_STATE] - if CONF_VERBOSE_FORMAT in user_input: - verbose_format = user_input[CONF_VERBOSE_FORMAT] - if CONF_DATE_FORMAT in user_input: - date_format = user_input[CONF_DATE_FORMAT] - data_schema = OrderedDict() - data_schema[vol.Required(CONF_NAME, default=name)] = str - data_schema[vol.Required(CONF_FREQUENCY, default=frequency)] = vol.In( - FREQUENCY_OPTIONS - ) - data_schema[vol.Required(CONF_ICON_NORMAL, default=icon_normal)] = str - data_schema[vol.Required(CONF_ICON_TOMORROW, default=icon_tomorrow)] = str - data_schema[vol.Required(CONF_ICON_TODAY, default=icon_today)] = str - data_schema[vol.Required(CONF_VERBOSE_STATE, default=verbose_state)] = bool - data_schema[vol.Required(CONF_VERBOSE_FORMAT, default=verbose_format)] = str - data_schema[vol.Required(CONF_DATE_FORMAT, default=date_format)] = str - return self.async_show_form( - step_id="user", data_schema=vol.Schema(data_schema), errors=self._errors - ) - - async def async_step_annual_group( - self, user_input={} - ): # pylint: disable=dangerous-default-value - """ - - C O N F I G U R A T I O N S T E P A N N U A L O R G R O U P - - """ - self._errors = {} - updates = {} - if user_input is not None and user_input != {}: - if self._data[CONF_FREQUENCY] in ANNUAL_FREQUENCY: - updates[CONF_DATE] = user_input[CONF_DATE] - if not is_month_day(user_input[CONF_DATE]): - self._errors["base"] = "month_day" - else: - updates[CONF_ENTITIES] = string_to_list(user_input[CONF_ENTITIES]) - checked = True - for entity in updates[CONF_ENTITIES]: - try: - self.hass.states.get(entity).attributes.get(ATTR_NEXT_DATE) - except: - checked = False - if not checked: - self._errors["base"] = "entities" - if self._errors == {}: - # Remember Frequency - self._data.update(updates) - # Call last step - return self.async_create_entry( - title=self._data["name"], data=self._data - ) - return await self._show_annual_group_form(user_input) - - async def _show_annual_group_form(self, user_input): - """Configuration STEP 2 - Annual or Group (no days) - SHOW FORM""" - # Defaults - date = "" - entities = "" - if user_input is not None: - if CONF_DATE in user_input: - date = user_input[CONF_DATE] - if CONF_ENTITIES in user_input: - entities = user_input[CONF_ENTITIES] - data_schema = OrderedDict() - if self._data[CONF_FREQUENCY] in ANNUAL_FREQUENCY: - data_schema[vol.Required(CONF_DATE, default=date)] = str - else: - data_schema[vol.Required(CONF_ENTITIES, default=entities)] = str - return self.async_show_form( - step_id="annual_group", - data_schema=vol.Schema(data_schema), - errors=self._errors, - ) - - async def async_step_detail( - self, user_input={} - ): # pylint: disable=dangerous-default-value - """ - - C O N F I G U R A T I O N S T E P 2 - - """ - self._errors = {} - if user_input is not None and user_input != {}: - day_selected = False - detail_info = {} - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - detail_info[CONF_FORCE_WEEK_NUMBERS] = user_input[ - CONF_FORCE_WEEK_NUMBERS - ] - detail_info[CONF_COLLECTION_DAYS] = [] - for day in WEEKDAYS: - if user_input[f"collection_days_{day.lower()}"]: - day_selected = True - detail_info[CONF_COLLECTION_DAYS].append(day) - if day_selected: - # Remember Detail - self._data.update(detail_info) - # Call last step - return await self.async_step_final() - else: - self._errors["base"] = "days" - return await self._show_detail_form(user_input) - - async def _show_detail_form(self, user_input): - """Configuration STEP 2 - SHOW FORM""" - data_schema = OrderedDict() - for day in WEEKDAYS: - data_schema[ - vol.Required( - f"collection_days_{day.lower()}", - default=bool( - user_input is not None - and user_input != {} - and user_input[f"collection_days_{day.lower()}"] - ), - ) - ] = bool - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - data_schema[ - vol.Required( - CONF_FORCE_WEEK_NUMBERS, - default=bool( - user_input is not None - and user_input != {} - and user_input[CONF_FORCE_WEEK_NUMBERS] - ), - ) - ] = bool - return self.async_show_form( - step_id="detail", data_schema=vol.Schema(data_schema), errors=self._errors - ) - - async def async_step_final( - self, user_input={} - ): # pylint: disable=dangerous-default-value - """ - - C O N F I G U R A T I O N S T E P 3 - - """ - self._errors = {} - if user_input is not None and user_input != {}: - final_info = {} - final_info[CONF_FIRST_MONTH] = user_input[CONF_FIRST_MONTH] - final_info[CONF_LAST_MONTH] = user_input[CONF_LAST_MONTH] - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - final_info[CONF_PERIOD] = user_input[CONF_PERIOD] - day_selected = False - final_info[CONF_WEEKDAY_ORDER_NUMBER] = [] - final_info[CONF_WEEK_ORDER_NUMBER] = [] - for i in range(5): - if self._data[CONF_FORCE_WEEK_NUMBERS]: - if user_input[f"week_order_number_{i+1}"]: - day_selected = True - final_info[CONF_WEEK_ORDER_NUMBER].append(i + 1) - else: - if user_input[f"weekday_order_number_{i+1}"]: - day_selected = True - final_info[CONF_WEEKDAY_ORDER_NUMBER].append(i + 1) - if not day_selected: - self._errors["base"] = CONF_WEEKDAY_ORDER_NUMBER - if self._data[CONF_FREQUENCY] in DAILY_FREQUENCY: - if is_date(user_input[CONF_FIRST_DATE]): - final_info[CONF_FIRST_DATE] = user_input[CONF_FIRST_DATE] - else: - self._errors["base"] = "date" - final_info[CONF_PERIOD] = user_input[CONF_PERIOD] - if self._data[CONF_FREQUENCY] in WEEKLY_FREQUENCY_X: - final_info[CONF_PERIOD] = user_input[CONF_PERIOD] - final_info[CONF_FIRST_WEEK] = user_input[CONF_FIRST_WEEK] - final_info[CONF_INCLUDE_DATES] = string_to_list( - user_input[CONF_INCLUDE_DATES] - ) - final_info[CONF_EXCLUDE_DATES] = string_to_list( - user_input[CONF_EXCLUDE_DATES] - ) - final_info[CONF_MOVE_COUNTRY_HOLIDAYS] = user_input[ - CONF_MOVE_COUNTRY_HOLIDAYS - ] - final_info[CONF_PROV] = user_input[CONF_PROV] - final_info[CONF_STATE] = user_input[CONF_STATE] - final_info[CONF_OBSERVED] = user_input[CONF_OBSERVED] - if not is_dates(final_info[CONF_INCLUDE_DATES]) or not is_dates( - final_info[CONF_EXCLUDE_DATES] - ): - self._errors["base"] = "date" - if self._errors == {}: - self._data.update(final_info) - return self.async_create_entry( - title=self._data["name"], data=self._data - ) - return await self._show_final_form(user_input) - - async def _show_final_form(self, user_input): - """Configuration STEP 3 - SHOW FORM""" - # Defaults - first_month = DEFAULT_FIRST_MONTH - last_month = DEFAULT_LAST_MONTH - first_date = "" - include_dates = "" - exclude_dates = "" - period = 1 - first_week = 1 - include_country_holidays = "" - prov = "" - state = "" - observed = True - if user_input is not None: - if CONF_FIRST_MONTH in user_input: - first_month = user_input[CONF_FIRST_MONTH] - if CONF_LAST_MONTH in user_input: - last_month = user_input[CONF_LAST_MONTH] - if CONF_FIRST_DATE in user_input: - first_date = user_input[CONF_FIRST_DATE] - if CONF_PERIOD in user_input: - period = user_input[CONF_PERIOD] - if CONF_FIRST_WEEK in user_input: - first_week = user_input[CONF_FIRST_WEEK] - if CONF_INCLUDE_DATES in user_input: - include_dates = user_input[CONF_INCLUDE_DATES] - if CONF_EXCLUDE_DATES in user_input: - exclude_dates = user_input[CONF_EXCLUDE_DATES] - if CONF_MOVE_COUNTRY_HOLIDAYS in user_input: - include_country_holidays = user_input[CONF_MOVE_COUNTRY_HOLIDAYS] - if CONF_PROV in user_input: - prov = user_input[CONF_PROV] - if CONF_STATE in user_input: - state = user_input[CONF_STATE] - if CONF_OBSERVED in user_input: - observed = user_input[CONF_OBSERVED] - - data_schema = OrderedDict() - data_schema[vol.Optional(CONF_FIRST_MONTH, default=first_month)] = vol.In( - MONTH_OPTIONS - ) - data_schema[vol.Optional(CONF_LAST_MONTH, default=last_month)] = vol.In( - MONTH_OPTIONS - ) - if self._data[CONF_FREQUENCY] in DAILY_FREQUENCY: - data_schema[vol.Required(CONF_PERIOD, default=period)] = vol.All( - vol.Coerce(int), vol.Range(min=1, max=52) - ) - data_schema[vol.Required(CONF_FIRST_DATE, default=first_date)] = str - if self._data[CONF_FREQUENCY] in WEEKLY_FREQUENCY_X: - data_schema[vol.Required(CONF_PERIOD, default=period)] = vol.All( - vol.Coerce(int), vol.Range(min=1, max=52) - ) - data_schema[vol.Required(CONF_FIRST_WEEK, default=first_week)] = vol.All( - vol.Coerce(int), vol.Range(min=1, max=52) - ) - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - data_schema[vol.Required(CONF_PERIOD, default=period)] = vol.All( - vol.Coerce(int), vol.Range(min=1, max=12) - ) - for i in range(5): - if self._data[CONF_FORCE_WEEK_NUMBERS]: - data_schema[ - vol.Required( - f"week_order_number_{i+1}", - default=bool( - user_input is not None - and user_input != {} - and user_input[f"week_order_number_{i+1}"] - ), - ) - ] = bool - else: - data_schema[ - vol.Required( - f"weekday_order_number_{i+1}", - default=bool( - user_input is not None - and user_input != {} - and user_input[f"weekday_order_number_{i+1}"] - ), - ) - ] = bool - data_schema[vol.Optional(CONF_INCLUDE_DATES, default=include_dates)] = str - data_schema[vol.Optional(CONF_EXCLUDE_DATES, default=exclude_dates)] = str - data_schema[ - vol.Optional(CONF_MOVE_COUNTRY_HOLIDAYS, default=include_country_holidays) - ] = vol.In(COUNTRY_CODES) - data_schema[vol.Optional(CONF_PROV, default=prov)] = str - data_schema[vol.Optional(CONF_STATE, default=state)] = str - data_schema[vol.Optional(CONF_OBSERVED, default=observed)] = bool - return self.async_show_form( - step_id="final", data_schema=vol.Schema(data_schema), errors=self._errors - ) - - async def async_step_import(self, user_input): # pylint: disable=unused-argument - """Import a config entry. - Special type of import, we're not actually going to store any data. - Instead, we're going to rely on the values that are in config file. - """ - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return self.async_create_entry(title="configuration.yaml", data={}) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - if config_entry.options.get("unique_id", None) is not None: - return OptionsFlowHandler(config_entry) - else: - return EmptyOptions(config_entry) - - -def is_month_day(date): - """Validates mm/dd format""" - try: - date = datetime.strptime(date, "%m/%d") - return True - except ValueError: - return False - - -def is_date(date): - """Validates yyyy-mm-dd format""" - if date == "": - return True - try: - datetime.strptime(date, "%Y-%m-%d") - return True - except ValueError: - return False - - -def string_to_list(string): - if string is None or string == "": - return [] - return list(map(lambda x: x.strip(), string.split(","))) - - -def is_dates(dates): - """Validates list of dates (yyyy-mm-dd, yyyy-mm-dd)""" - if dates == []: - return True - check = True - for date in dates: - if not is_date(date): - check = False - return check - - -""" - -O P T I O N S F L O W - -""" - - -class OptionsFlowHandler(config_entries.OptionsFlow): - def __init__(self, config_entry): - self.config_entry = config_entry - self._data = config_entry.options - - async def async_step_init(self, user_input=None): - """ - - O P T I O N S S T E P 1 - - """ - self._errors = {} - if user_input is not None: - # Remember Frequency - self._data.update(user_input) - # Call next step - if ( - user_input[CONF_FREQUENCY] in ANNUAL_FREQUENCY - or user_input[CONF_FREQUENCY] in GROUP_FREQUENCY - ): - return await self.async_step_annual_group() - elif user_input[CONF_FREQUENCY] in DAILY_FREQUENCY: - return await self.async_step_final() - else: - return await self.async_step_detail() - return await self._show_init_form(user_input) - return await self._show_init_form(user_input) - - async def _show_init_form(self, user_input): - """Options STEP 1 - SHOW FORM""" - # Defaults - data_schema = OrderedDict() - data_schema[ - vol.Required( - CONF_FREQUENCY, default=self.config_entry.options.get(CONF_FREQUENCY) - ) - ] = vol.In(FREQUENCY_OPTIONS) - data_schema[ - vol.Required( - CONF_ICON_NORMAL, - default=self.config_entry.options.get( - CONF_ICON_NORMAL, DEFAULT_ICON_NORMAL - ), - ) - ] = str - data_schema[ - vol.Required( - CONF_ICON_TOMORROW, - default=self.config_entry.options.get( - CONF_ICON_TOMORROW, DEFAULT_ICON_TOMORROW - ), - ) - ] = str - data_schema[ - vol.Required( - CONF_ICON_TODAY, - default=self.config_entry.options.get( - CONF_ICON_TODAY, DEFAULT_ICON_TODAY - ), - ) - ] = str - data_schema[ - vol.Required( - CONF_VERBOSE_STATE, - default=self.config_entry.options.get(CONF_VERBOSE_STATE, False), - ) - ] = bool - data_schema[ - vol.Required( - CONF_VERBOSE_FORMAT, - default=self.config_entry.options.get( - CONF_VERBOSE_FORMAT, DEFAULT_VERBOSE_FORMAT - ), - ) - ] = str - data_schema[ - vol.Required( - CONF_DATE_FORMAT, - default=self.config_entry.options.get( - CONF_DATE_FORMAT, DEFAULT_DATE_FORMAT - ), - ) - ] = str - return self.async_show_form( - step_id="init", data_schema=vol.Schema(data_schema), errors=self._errors - ) - - async def async_step_annual_group( - self, user_input={} - ): # pylint: disable=dangerous-default-value - """ - - O P T I O N S S T E P 2 F O R A N N U A L O R G R O U P - - """ - self._errors = {} - updates = {} - if user_input is not None and user_input != {}: - if self._data[CONF_FREQUENCY] in ANNUAL_FREQUENCY: - updates[CONF_DATE] = user_input[CONF_DATE] - if not is_month_day(user_input[CONF_DATE]): - self._errors["base"] = "month_day" - else: - updates[CONF_ENTITIES] = string_to_list(user_input[CONF_ENTITIES]) - checked = True - for entity in updates[CONF_ENTITIES]: - try: - self.hass.states.get(entity).attributes.get(ATTR_NEXT_DATE) - except: - checked = False - if not checked: - self._errors["base"] = "entities" - if self._errors == {}: - # Remember Frequency - clean_optional(self._data, CONF_DATE) - self._data.update(updates) - # Call last step - return self.async_create_entry(title="", data=self._data) - return await self._show_annual_group_form(user_input) - - async def _show_annual_group_form(self, user_input): - """Configuration STEP 2 for Annual or Group - SHOW FORM""" - # Defaults - data_schema = OrderedDict() - if self._data[CONF_FREQUENCY] in ANNUAL_FREQUENCY: - data_schema[ - vol.Optional( - CONF_DATE, default=self.config_entry.options.get(CONF_DATE) - ) - ] = str - else: - data_schema[ - vol.Required( - CONF_ENTITIES, - default=",".join(self.config_entry.options.get(CONF_ENTITIES)), - ) - ] = str - return self.async_show_form( - step_id="annual_group", - data_schema=vol.Schema(data_schema), - errors=self._errors, - ) - - async def async_step_detail( - self, user_input={} - ): # pylint: disable=dangerous-default-value - """ - - O P T I O N S S T E P 2 - - """ - self._errors = {} - if user_input is not None and user_input != {}: - day_selected = False - detail_info = {} - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - detail_info[CONF_FORCE_WEEK_NUMBERS] = user_input[ - CONF_FORCE_WEEK_NUMBERS - ] - detail_info[CONF_COLLECTION_DAYS] = [] - for day in WEEKDAYS: - if user_input[f"collection_days_{day.lower()}"]: - day_selected = True - detail_info[CONF_COLLECTION_DAYS].append(day) - if day_selected: - # Remember Detail - self._data.update(detail_info) - # Call last step - return await self.async_step_final() - else: - self._errors["base"] = "days" - return await self._show_detail_form(user_input) - - async def _show_detail_form(self, user_input): - """Configuration STEP 2 - SHOW FORM""" - data_schema = OrderedDict() - for day in WEEKDAYS: - data_schema[ - vol.Required( - f"collection_days_{day.lower()}", - default=bool( - day.lower() - in self.config_entry.options.get(CONF_COLLECTION_DAYS) - ), - ) - ] = bool - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - data_schema[ - vol.Required( - CONF_FORCE_WEEK_NUMBERS, - default=bool( - self.config_entry.options.get(CONF_FORCE_WEEK_NUMBERS, False) - ), - ) - ] = bool - return self.async_show_form( - step_id="detail", data_schema=vol.Schema(data_schema), errors=self._errors - ) - - async def async_step_final( - self, user_input={} - ): # pylint: disable=dangerous-default-value - """ - - C O N F I G U R A T I O N S T E P 3 - - """ - self._errors = {} - if user_input is not None and user_input != {}: - final_info = {} - final_info[CONF_FIRST_MONTH] = user_input[CONF_FIRST_MONTH] - final_info[CONF_LAST_MONTH] = user_input[CONF_LAST_MONTH] - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - final_info[CONF_PERIOD] = user_input[CONF_PERIOD] - day_selected = False - final_info[CONF_WEEKDAY_ORDER_NUMBER] = [] - final_info[CONF_WEEK_ORDER_NUMBER] = [] - for i in range(5): - if self._data[CONF_FORCE_WEEK_NUMBERS]: - if user_input[f"week_order_number_{i+1}"]: - day_selected = True - final_info[CONF_WEEK_ORDER_NUMBER].append(i + 1) - else: - if user_input[f"weekday_order_number_{i+1}"]: - day_selected = True - final_info[CONF_WEEKDAY_ORDER_NUMBER].append(i + 1) - if not day_selected: - self._errors["base"] = CONF_WEEKDAY_ORDER_NUMBER - final_info[CONF_INCLUDE_DATES] = string_to_list( - user_input[CONF_INCLUDE_DATES] - ) - final_info[CONF_EXCLUDE_DATES] = string_to_list( - user_input[CONF_EXCLUDE_DATES] - ) - if not is_dates(final_info[CONF_INCLUDE_DATES]) or not is_dates( - final_info[CONF_EXCLUDE_DATES] - ): - self._errors["base"] = "date" - final_info[CONF_MOVE_COUNTRY_HOLIDAYS] = user_input[ - CONF_MOVE_COUNTRY_HOLIDAYS - ] - final_info[CONF_PROV] = user_input[CONF_PROV] - final_info[CONF_STATE] = user_input[CONF_STATE] - final_info[CONF_OBSERVED] = user_input[CONF_OBSERVED] - - if self._data[CONF_FREQUENCY] in DAILY_FREQUENCY: - if is_date(user_input[CONF_FIRST_DATE]): - final_info[CONF_FIRST_DATE] = user_input[CONF_FIRST_DATE] - else: - self._errors["base"] = "date" - final_info[CONF_PERIOD] = user_input[CONF_PERIOD] - if self._data[CONF_FREQUENCY] in WEEKLY_FREQUENCY_X: - final_info[CONF_PERIOD] = user_input[CONF_PERIOD] - final_info[CONF_FIRST_WEEK] = user_input[CONF_FIRST_WEEK] - if self._errors == {}: - clean_optional(self._data, CONF_FIRST_MONTH) - clean_optional(self._data, CONF_LAST_MONTH) - clean_optional(self._data, CONF_INCLUDE_DATES) - clean_optional(self._data, CONF_EXCLUDE_DATES) - clean_optional(self._data, CONF_MOVE_COUNTRY_HOLIDAYS) - clean_optional(self._data, CONF_PROV) - clean_optional(self._data, CONF_STATE) - clean_optional(self._data, CONF_OBSERVED) - # _LOGGER.debug("final_info %s",final_info) - self._data.update(final_info) - return self.async_create_entry(title="", data=self._data) - return await self._show_final_form(user_input) - - async def _show_final_form(self, user_input): - """Configuration STEP 3 - SHOW FORM""" - data_schema = OrderedDict() - data_schema[ - vol.Optional( - CONF_FIRST_MONTH, - default=self.config_entry.options.get(CONF_FIRST_MONTH), - ) - ] = vol.In(MONTH_OPTIONS) - data_schema[ - vol.Optional( - CONF_LAST_MONTH, default=self.config_entry.options.get(CONF_LAST_MONTH) - ) - ] = vol.In(MONTH_OPTIONS) - if self._data[CONF_FREQUENCY] in WEEKLY_FREQUENCY_X: - data_schema[ - vol.Required( - CONF_PERIOD, default=self.config_entry.options.get(CONF_PERIOD) - ) - ] = vol.All(vol.Coerce(int), vol.Range(min=1, max=52)) - data_schema[ - vol.Required( - CONF_FIRST_WEEK, - default=self.config_entry.options.get(CONF_FIRST_WEEK), - ) - ] = vol.All(vol.Coerce(int), vol.Range(min=1, max=52)) - if self._data[CONF_FREQUENCY] in MONTHLY_FREQUENCY: - data_schema[ - vol.Required( - CONF_PERIOD, default=self.config_entry.options.get(CONF_PERIOD) - ) - ] = vol.All(vol.Coerce(int), vol.Range(min=1, max=12)) - for i in range(5): - if self._data[CONF_FORCE_WEEK_NUMBERS]: - data_schema[ - vol.Required( - f"week_order_number_{i+1}", - default=bool( - (i + 1) - in self.config_entry.options.get(CONF_WEEK_ORDER_NUMBER) - ), - ) - ] = bool - else: - data_schema[ - vol.Required( - f"weekday_order_number_{i+1}", - default=bool( - (i + 1) - in self.config_entry.options.get( - CONF_WEEKDAY_ORDER_NUMBER - ) - ), - ) - ] = bool - if self._data[CONF_FREQUENCY] in DAILY_FREQUENCY: - data_schema[ - vol.Required( - CONF_PERIOD, default=self.config_entry.options.get(CONF_PERIOD) - ) - ] = vol.All(vol.Coerce(int), vol.Range(min=1, max=52)) - data_schema[ - vol.Required( - CONF_FIRST_DATE, - default=self.config_entry.options.get(CONF_FIRST_DATE), - ) - ] = str - data_schema[ - vol.Optional( - CONF_INCLUDE_DATES, - default=",".join(self.config_entry.options.get(CONF_INCLUDE_DATES)), - ) - ] = str - data_schema[ - vol.Optional( - CONF_EXCLUDE_DATES, - default=",".join(self.config_entry.options.get(CONF_EXCLUDE_DATES)), - ) - ] = str - data_schema[ - vol.Optional( - CONF_MOVE_COUNTRY_HOLIDAYS, - default=self.config_entry.options.get(CONF_MOVE_COUNTRY_HOLIDAYS, ""), - ) - ] = vol.In(COUNTRY_CODES) - data_schema[ - vol.Optional( - CONF_PROV, default=self.config_entry.options.get(CONF_PROV, "") - ) - ] = str - data_schema[ - vol.Optional( - CONF_STATE, default=self.config_entry.options.get(CONF_STATE, "") - ) - ] = str - data_schema[ - vol.Optional( - CONF_OBSERVED, - default=self.config_entry.options.get(CONF_OBSERVED, True), - ) - ] = bool - return self.async_show_form( - step_id="final", data_schema=vol.Schema(data_schema), errors=self._errors - ) - - -class EmptyOptions(config_entries.OptionsFlow): - def __init__(self, config_entry): - self.config_entry = config_entry diff --git a/custom_components/garbage_collection/const.py b/custom_components/garbage_collection/const.py deleted file mode 100644 index 9d677f8..0000000 --- a/custom_components/garbage_collection/const.py +++ /dev/null @@ -1,227 +0,0 @@ -import voluptuous as vol -from datetime import datetime, date -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, WEEKDAYS, CONF_ENTITIES - -"""Constants for garbage_collection.""" -# Base component constants -DOMAIN = "garbage_collection" -DOMAIN_DATA = f"{DOMAIN}_data" -VERSION = "2.18" -PLATFORM = "sensor" -ISSUE_URL = "https://github.com/bruxy70/Garbage-Collection/issues" -ATTRIBUTION = "Data from this is provided by garbage_collection." - -ATTR_NEXT_DATE = "next_date" -ATTR_DAYS = "days" - -# Device classes -BINARY_SENSOR_DEVICE_CLASS = "connectivity" - -# Configuration -CONF_SENSOR = "sensor" -CONF_ENABLED = "enabled" -CONF_FREQUENCY = "frequency" -CONF_ICON_NORMAL = "icon_normal" -CONF_ICON_TODAY = "icon_today" -CONF_ICON_TOMORROW = "icon_tomorrow" -CONF_VERBOSE_STATE = "verbose_state" -CONF_FIRST_MONTH = "first_month" -CONF_LAST_MONTH = "last_month" -CONF_COLLECTION_DAYS = "collection_days" -CONF_FORCE_WEEK_NUMBERS = "force_week_order_numbers" -CONF_WEEKDAY_ORDER_NUMBER = "weekday_order_number" -CONF_WEEK_ORDER_NUMBER = "week_order_number" -CONF_DATE = "date" -CONF_EXCLUDE_DATES = "exclude_dates" -CONF_INCLUDE_DATES = "include_dates" -CONF_MOVE_COUNTRY_HOLIDAYS = "move_country_holidays" -CONF_PROV = "prov" -CONF_STATE = "state" -CONF_OBSERVED = "observed" -CONF_PERIOD = "period" -CONF_FIRST_WEEK = "first_week" -CONF_FIRST_DATE = "first_date" -CONF_SENSORS = "sensors" -CONF_VERBOSE_FORMAT = "verbose_format" -CONF_DATE_FORMAT = "date_format" - -# Defaults -DEFAULT_NAME = DOMAIN -DEFAULT_FIRST_MONTH = "jan" -DEFAULT_LAST_MONTH = "dec" -DEFAULT_FREQUENCY = "weekly" -DEFAULT_PERIOD = 1 -DEFAULT_FIRST_WEEK = 1 -DEFAULT_VERBOSE_STATE = False -DEFAULT_DATE_FORMAT = "%d-%b-%Y" -DEFAULT_VERBOSE_FORMAT = "on {date}, in {days} days" - -# Icons -DEFAULT_ICON_NORMAL = "mdi:trash-can" -DEFAULT_ICON_TODAY = "mdi:delete-restore" -DEFAULT_ICON_TOMORROW = "mdi:delete-circle" -ICON = DEFAULT_ICON_NORMAL - -# States -STATE_TODAY = "today" -STATE_TOMORROW = "tomorrow" - -FREQUENCY_OPTIONS = [ - "weekly", - "even-weeks", - "odd-weeks", - "every-n-weeks", - "every-n-days", - "monthly", - "annual", - "group", -] - -MONTH_OPTIONS = [ - "jan", - "feb", - "mar", - "apr", - "may", - "jun", - "jul", - "aug", - "sep", - "oct", - "nov", - "dec", -] - -COUNTRY_CODES = [ - "", - "AR", - "AT", - "AU", - "AW", - "BE", - "BG", - "BR", - "BY", - "CA", - "CH", - "CO", - "CZ", - "DE", - "DK", - "DO", - "ECB", - "EE", - "ES", - "FI", - "FRA", - "HR", - "HU", - "IE", - "IND", - "IS", - "IT", - "JP", - "KE", - "LT", - "LU", - "MX", - "NG", - "NI", - "NL", - "NO", - "NZ", - "PE", - "PL", - "PT", - "PTE", - "RU", - "SE", - "SI", - "SK", - "UA", - "UK", - "US", - "ZA", -] - - -def date_text(value): - if value is None or value == "": - return "" - try: - return datetime.strptime(value, "%Y-%m-%d").date().strftime("%Y-%m-%d") - except ValueError: - raise vol.Invalid(f"Invalid date: {value}") - - -def month_day_text(value): - if value is None or value == "": - return "" - try: - return datetime.strptime(value, "%m/%d").date().strftime("%m/%d") - except ValueError: - raise vol.Invalid(f"Invalid date: {value}") - - -SENSOR_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FREQUENCY): vol.In(FREQUENCY_OPTIONS), - vol.Optional(CONF_COLLECTION_DAYS): vol.All(cv.ensure_list, [vol.In(WEEKDAYS)]), - vol.Optional(CONF_FIRST_MONTH, default=DEFAULT_FIRST_MONTH): vol.In( - MONTH_OPTIONS - ), - vol.Optional(CONF_LAST_MONTH, default=DEFAULT_LAST_MONTH): vol.In( - MONTH_OPTIONS - ), - vol.Optional(CONF_WEEKDAY_ORDER_NUMBER, default=[1]): vol.All( - cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(min=1, max=5))] - ), - vol.Optional(CONF_WEEK_ORDER_NUMBER, default=[]): vol.All( - cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(min=1, max=5))] - ), - vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): vol.All( - vol.Coerce(int), vol.Range(min=1, max=52) - ), - vol.Optional(CONF_FIRST_WEEK, default=DEFAULT_FIRST_WEEK): vol.All( - vol.Coerce(int), vol.Range(min=1, max=52) - ), - vol.Optional(CONF_FIRST_DATE): date_text, - vol.Optional(CONF_DATE): month_day_text, - vol.Optional(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_INCLUDE_DATES, default=[]): vol.All( - cv.ensure_list, [date_text] - ), - vol.Optional(CONF_EXCLUDE_DATES, default=[]): vol.All( - cv.ensure_list, [date_text] - ), - vol.Optional(CONF_MOVE_COUNTRY_HOLIDAYS): vol.In(COUNTRY_CODES), - vol.Optional(CONF_PROV): cv.string, - vol.Optional(CONF_STATE): cv.string, - vol.Optional(CONF_OBSERVED, default=True): bool, - vol.Optional(CONF_ICON_NORMAL, default=DEFAULT_ICON_NORMAL): cv.icon, - vol.Optional(CONF_ICON_TODAY, default=DEFAULT_ICON_TODAY): cv.icon, - vol.Optional(CONF_ICON_TOMORROW, default=DEFAULT_ICON_TOMORROW): cv.icon, - vol.Optional(CONF_VERBOSE_STATE, default=DEFAULT_VERBOSE_STATE): cv.boolean, - vol.Optional(CONF_DATE_FORMAT, default=DEFAULT_DATE_FORMAT): cv.string, - vol.Optional(CONF_VERBOSE_FORMAT, default=DEFAULT_VERBOSE_FORMAT): cv.string, - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA])} - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -WEEKLY_FREQUENCY = ["weekly", "even-weeks", "odd-weeks"] -WEEKLY_FREQUENCY_X = ["every-n-weeks"] -DAILY_FREQUENCY = ["every-n-days"] -MONTHLY_FREQUENCY = ["monthly"] -ANNUAL_FREQUENCY = ["annual"] -GROUP_FREQUENCY = ["group"] diff --git a/custom_components/garbage_collection/manifest.json b/custom_components/garbage_collection/manifest.json deleted file mode 100644 index 152c54d..0000000 --- a/custom_components/garbage_collection/manifest.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "domain": "garbage_collection", - "name": "Garbage Collection", - "documentation": "https://github.com/bruxy70/Garbage-Collection/", - "dependencies": [], - "config_flow": true, - "codeowners": [ - "@bruxy70" - ], - "requirements": [ - "datetime", - "python-dateutil>=2.8.1", - "integrationhelper>=0.2.2", - "holidays>=0.10.1", - "typing", - "uuid", - "voluptuous" - ], - "homeassistant": "0.96.0" -} \ No newline at end of file diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py deleted file mode 100644 index 76300a2..0000000 --- a/custom_components/garbage_collection/sensor.py +++ /dev/null @@ -1,496 +0,0 @@ -"""Sensor platform for garbage_collection.""" -from homeassistant.helpers.entity import Entity -import homeassistant.util.dt as dt_util -import holidays -import logging -import locale -from datetime import datetime, date, timedelta -from dateutil.relativedelta import relativedelta -from homeassistant.core import HomeAssistant, State -from typing import List, Any - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) -THROTTLE_INTERVAL = timedelta(seconds=60) -ATTR_NEXT_DATE = "next_date" -ATTR_DAYS = "days" - -from homeassistant.const import CONF_NAME, WEEKDAYS, CONF_ENTITIES -from .const import ( - ATTRIBUTION, - DEFAULT_NAME, - DOMAIN, - CONF_SENSOR, - CONF_ENABLED, - CONF_FREQUENCY, - CONF_ICON_NORMAL, - CONF_ICON_TODAY, - CONF_ICON_TOMORROW, - CONF_VERBOSE_STATE, - CONF_VERBOSE_FORMAT, - CONF_DATE_FORMAT, - DEFAULT_DATE_FORMAT, - DEFAULT_VERBOSE_FORMAT, - CONF_FIRST_MONTH, - CONF_LAST_MONTH, - CONF_COLLECTION_DAYS, - CONF_FORCE_WEEK_NUMBERS, - CONF_WEEKDAY_ORDER_NUMBER, - CONF_WEEK_ORDER_NUMBER, - CONF_DATE, - CONF_EXCLUDE_DATES, - CONF_INCLUDE_DATES, - CONF_MOVE_COUNTRY_HOLIDAYS, - CONF_PROV, - CONF_STATE, - CONF_OBSERVED, - CONF_PERIOD, - CONF_FIRST_WEEK, - CONF_FIRST_DATE, - CONF_SENSORS, - MONTH_OPTIONS, - FREQUENCY_OPTIONS, - STATE_TODAY, - STATE_TOMORROW, -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -): # pylint: disable=unused-argument - """Setup sensor platform.""" - async_add_entities([GarbageCollection(hass, discovery_info)], True) - - -async def async_setup_entry(hass, config_entry, async_add_devices): - """Setup sensor platform.""" - async_add_devices([GarbageCollection(hass, config_entry.data)], True) - - -def nth_week_date(n: int, date_of_month: date, collection_day: int) -> date: - """Find weekday in the nth week of the month""" - first_of_month = date(date_of_month.year, date_of_month.month, 1) - month_starts_on = first_of_month.weekday() - return first_of_month + relativedelta( - days=collection_day - month_starts_on + (n - 1) * 7 - ) - - -def nth_weekday_date(n: int, date_of_month: date, collection_day: int) -> date: - """Find nth weekday of the month""" - first_of_month = date(date_of_month.year, date_of_month.month, 1) - month_starts_on = first_of_month.weekday() - # 1st of the month is before the day of collection - # (so 1st collection week the week when month starts) - if collection_day >= month_starts_on: - return first_of_month + relativedelta( - days=collection_day - month_starts_on + (n - 1) * 7 - ) - else: # Next week - return first_of_month + relativedelta( - days=7 - month_starts_on + collection_day + (n - 1) * 7 - ) - - -def to_date(day: Any) -> date: - if day is None: - return None - if type(day) == date: - return day - if type(day) == datetime: - return day.date() - return date.fromisoformat(day) - - -def to_dates(dates: List[Any]) -> List[date]: - # Convert list of text to datetimes, if not already datetimes - converted = [] - for day in dates: - try: - converted.append(to_date(day)) - except ValueError: - continue - return converted - - -class GarbageCollection(Entity): - """GarbageCollection Sensor class.""" - - def __init__(self, hass, config): - self.config = config - self.__name = config.get(CONF_NAME) - self.__frequency = config.get(CONF_FREQUENCY) - self.__collection_days = config.get(CONF_COLLECTION_DAYS) - first_month = config.get(CONF_FIRST_MONTH) - if first_month in MONTH_OPTIONS: - self.__first_month = MONTH_OPTIONS.index(first_month) + 1 - else: - self.__first_month = 1 - last_month = config.get(CONF_LAST_MONTH) - if last_month in MONTH_OPTIONS: - self.__last_month = MONTH_OPTIONS.index(last_month) + 1 - else: - self.__last_month = 12 - self._weekday_order_numbers = config.get(CONF_WEEKDAY_ORDER_NUMBER) - self._week_order_numbers = config.get(CONF_WEEK_ORDER_NUMBER) - self.__monthly_force_week_numbers = bool( - self._week_order_numbers is not None and len(self._week_order_numbers) != 0 - ) - self.__include_dates = to_dates(config.get(CONF_INCLUDE_DATES, [])) - self.__exclude_dates = to_dates(config.get(CONF_EXCLUDE_DATES, [])) - country_holidays = config.get(CONF_MOVE_COUNTRY_HOLIDAYS) - self.__holidays = [] - holidays_log = "" - if country_holidays is not None and country_holidays != "": - today = dt_util.now().date() - this_year = today.year - years = [this_year, this_year + 1] - prov = config.get(CONF_PROV) - state = config.get(CONF_STATE) - observed = config.get(CONF_OBSERVED, True) - _LOGGER.debug( - "(%s) Country Holidays with parameters: country: %s, prov: %s, state: %s, observed: %s", - self.__name, - country_holidays, - prov, - state, - observed, - ) - kwargs = {"years": years} - if state is not None and state != "": - kwargs["state"] = state - if prov is not None and prov != "": - kwargs["prov"] = prov - if observed is not None and type(observed) == bool and observed == False: - kwargs["observed"] = observed - hol = holidays.CountryHoliday(country_holidays, **kwargs).items() - try: - for date, name in hol: - if date >= today: - self.__holidays.append(date) - holidays_log += f"\n {date}: {name}" - except KeyError: - _LOGGER.error( - "(%s) Invalid country code (%s)", self.__name, country_holidays - ) - _LOGGER.debug("(%s) Found these holidays: %s", self.__name, holidays_log) - self.__period = config.get(CONF_PERIOD) - self.__first_week = config.get(CONF_FIRST_WEEK) - self.__first_date = to_date(config.get(CONF_FIRST_DATE)) - self.__next_date = None - self.__last_updated = None - self.__days = 0 - self.__date = config.get(CONF_DATE) - self.__entities = config.get(CONF_ENTITIES) - self.__verbose_state = config.get(CONF_VERBOSE_STATE) - self.__state = "" if bool(self.__verbose_state) else 2 - self.__icon_normal = config.get(CONF_ICON_NORMAL) - self.__icon_today = config.get(CONF_ICON_TODAY) - self.__icon_tomorrow = config.get(CONF_ICON_TOMORROW) - self.__date_format = config.get(CONF_DATE_FORMAT, DEFAULT_DATE_FORMAT) - self.__verbose_format = config.get(CONF_VERBOSE_FORMAT, DEFAULT_VERBOSE_FORMAT) - self.__icon = self.__icon_normal - - @property - def unique_id(self): - """Return a unique ID to use for this sensor.""" - return self.config.get("unique_id", None) - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.config.get("unique_id", None))}, - "name": self.config.get("name"), - "manufacturer": "Garbage Collection", - } - - @property - def name(self): - """Return the name of the sensor.""" - return self.__name - - @property - def state(self): - """Return the state of the sensor.""" - return self.__state - - @property - def icon(self): - return self.__icon - - @property - def device_state_attributes(self): - """Return the state attributes.""" - res = {} - if self.__next_date is None: - res[ATTR_NEXT_DATE] = None - else: - res[ATTR_NEXT_DATE] = datetime( - self.__next_date.year, self.__next_date.month, self.__next_date.day - ).astimezone() - res[ATTR_DAYS] = self.__days - res["last_updated"] = self.__last_updated - return res - - def date_inside(self, dat: date) -> bool: - month = dat.month - if self.__first_month <= self.__last_month: - return bool(month >= self.__first_month and month <= self.__last_month) - else: - return bool(month <= self.__last_month or month >= self.__first_month) - - def __monthly_candidate(self, day1: date) -> date: - if self.__monthly_force_week_numbers: - for week_order_number in self._week_order_numbers: - candidate_date = nth_week_date( - week_order_number, day1, WEEKDAYS.index(self.__collection_days[0]), - ) - # date is today or in the future -> we have the date - if candidate_date >= day1: - return candidate_date - else: - for weekday_order_number in self._weekday_order_numbers: - candidate_date = nth_weekday_date( - weekday_order_number, - day1, - WEEKDAYS.index(self.__collection_days[0]), - ) - # date is today or in the future -> we have the date - if candidate_date >= day1: - return candidate_date - if day1.month == 12: - next_collection_month = date(day1.year + 1, 1, 1) - else: - next_collection_month = date(day1.year, day1.month + 1, 1) - if self.__monthly_force_week_numbers: - return nth_week_date( - self._week_order_numbers[0], - next_collection_month, - WEEKDAYS.index(self.__collection_days[0]), - ) - else: - return nth_weekday_date( - self._weekday_order_numbers[0], - next_collection_month, - WEEKDAYS.index(self.__collection_days[0]), - ) - - def find_candidate_date(self, day1: date) -> date: - """Find the next possible date starting from day1, - only based on calendar, not lookimg at include/exclude days""" - week = day1.isocalendar()[1] - weekday = day1.weekday() - year = day1.year - if self.__frequency in ["weekly", "even-weeks", "odd-weeks", "every-n-weeks"]: - # Everything except montthly - # convert to every-n-weeks - if self.__frequency == "weekly": - period = 1 - first_week = 1 - elif self.__frequency == "even-weeks": - period = 2 - first_week = 2 - elif self.__frequency == "odd-weeks": - period = 2 - first_week = 1 - else: - period = self.__period - first_week = self.__first_week - offset = -1 - if (week - first_week) % period == 0: # Collection this week - for day_name in self.__collection_days: - day_index = WEEKDAYS.index(day_name) - if day_index >= weekday: # Collection still did not happen - offset = day_index - weekday - break - if offset == -1: # look in following weeks - in_weeks = period - (week - first_week) % period - offset = ( - 7 * in_weeks - weekday + WEEKDAYS.index(self.__collection_days[0]) - ) - return day1 + relativedelta(days=offset) - elif self.__frequency == "every-n-days": - if self.__first_date is None or self.__period is None: - _LOGGER.error( - "(%s) Please configure first_date and period for every-n-days collection frequency.", - self.__name, - ) - return None - if (day1 - self.__first_date).days % self.__period == 0: - return day1 - offset = self.__period - ((day1 - self.__first_date).days % self.__period) - return day1 + relativedelta(days=offset) - elif self.__frequency == "monthly": - # Monthly - if self.__period is None or self.__period == 1: - return self.__monthly_candidate(day1) - else: - candidate_date = self.__monthly_candidate(day1) - while (candidate_date.month - self.__first_month) % self.__period != 0: - candidate_date = self.__monthly_candidate( - candidate_date + relativedelta(days=1) - ) - return candidate_date - elif self.__frequency == "annual": - # Annual - if self.__date is None: - _LOGGER.error( - "(%s) Please configure the date for annual collection frequency.", - self.__name, - ) - return None - conf_date = datetime.strptime(self.__date, "%m/%d").date() - candidate_date = date(year, conf_date.month, conf_date.day) - if candidate_date < day1: - candidate_date = date(year + 1, conf_date.month, conf_date.day) - return candidate_date - elif self.__frequency == "group": - if self.__entities is None: - _LOGGER.error("(%s) Please add entities for the group.", self.__name) - return None - candidate_date = None - for entity in self.__entities: - d = self.hass.states.get(entity).attributes.get(ATTR_NEXT_DATE).date() - if candidate_date is None or d < candidate_date: - candidate_date = d - return candidate_date - else: - _LOGGER.debug(f"({self.__name}) Unknown frequency {self.__frequency}") - return None - - def __insert_include_date(self, day1: date, next_date: date) -> date: - include_dates = list(filter(lambda date: date >= day1, self.__include_dates)) - if len(include_dates) > 0 and include_dates[0] < next_date: - _LOGGER.debug( - "(%s) Inserting include_date %s", self.__name, include_dates[0] - ) - return include_dates[0] - else: - return next_date - - def __skip_holiday(self, day: date) -> date: - return day + relativedelta(days=1) - - def ready_for_update(self) -> bool: - today = dt_util.now().date() - ready_for_update = bool( - self.__last_updated is None or self.__last_updated.date() != today - ) - if self.__frequency == "group": - members_ready = True - for entity in self.__entities: - if ( - self.hass.states.get(entity).attributes.get("last_updated").date() - != today - ): - members_ready = False - if ready_for_update and not members_ready: - ready_for_update = False - return ready_for_update - - def get_next_date(self, day1: date) -> date: - """Find the next date starting from day1.""" - first_day = day1 - i = 0 - while i < 365: - next_date = self.find_candidate_date(first_day) - while next_date in self.__holidays: - _LOGGER.debug( - "(%s) Skipping public holiday on %s", self.__name, next_date - ) - next_date = self.__skip_holiday(next_date) - next_date = self.__insert_include_date(first_day, next_date) - if next_date not in self.__exclude_dates: - return next_date - _LOGGER.debug("(%s) Skipping exclude_date %s", self.__name, next_date) - first_day = next_date + relativedelta(days=1) - i += 1 - _LOGGER.error("(%s) Cannot find any suitable date", self.__name) - return None - - async def async_update(self) -> None: - """Get the latest data and updates the states.""" - now = dt_util.now() - if not self.ready_for_update(): - # _LOGGER.debug( - # "(%s) Skipping the update, already did it today", - # self.__name) - return - _LOGGER.debug("(%s) Calling update", self.__name) - today = now.date() - year = today.year - month = today.month - self.__last_updated = now - if self.date_inside(today): - next_date = self.get_next_date(today) - if next_date is not None: - if not self.date_inside(next_date): - if self.__first_month <= self.__last_month: - next_year = date(year + 1, self.__first_month, 1) - next_date = self.get_next_date(next_year) - _LOGGER.debug( - "(%s) Did not find a date this year, " - "lookig at next year", - self.__name, - ) - else: - next_year = date(year, self.__first_month, 1) - next_date = self.get_next_date(next_year) - _LOGGER.debug( - "(%s) Date not within the range, " - "searching again from %s", - self.__name, - MONTH_OPTIONS[self.__first_month - 1], - ) - else: - if self.__first_month <= self.__last_month and month > self.__last_month: - next_year = date(year + 1, self.__first_month, 1) - next_date = self.get_next_date(next_year) - _LOGGER.debug( - "(%s) Date outside range, lookig at next year", self.__name - ) - else: - next_year = date(year, self.__first_month, 1) - next_date = self.get_next_date(next_year) - _LOGGER.debug( - "(%s) Current date is outside of the range, " - "starting from first month", - self.__name, - ) - self.__next_date = next_date - if next_date is not None: - self.__days = (next_date - today).days - next_date_txt = next_date.strftime(self.__date_format) - _LOGGER.debug( - "(%s) Found next date: %s, that is in %d days", - self.__name, - next_date_txt, - self.__days, - ) - if self.__days > 1: - if bool(self.__verbose_state): - self.__state = self.__verbose_format.format( - date=next_date_txt, days=self.__days - ) - # self.__state = "on_date" - else: - self.__state = 2 - self.__icon = self.__icon_normal - else: - if self.__days == 0: - if bool(self.__verbose_state): - self.__state = STATE_TODAY - else: - self.__state = self.__days - self.__icon = self.__icon_today - elif self.__days == 1: - if bool(self.__verbose_state): - self.__state = STATE_TOMORROW - else: - self.__state = self.__days - self.__icon = self.__icon_tomorrow - else: - self.__days = None diff --git a/custom_components/hacs/.translations/da.json b/custom_components/hacs/.translations/da.json deleted file mode 100644 index 8e45229..0000000 --- a/custom_components/hacs/.translations/da.json +++ /dev/null @@ -1,191 +0,0 @@ -{ - "common": { - "about": "Om", - "appdaemon_apps": "AppDaemon-apps", - "background_task": "Baggrundsopgave kører. Denne side vil genindlæses automatisk.", - "check_log_file": "Tjek din logfil for flere detaljer.", - "continue": "Fortsæt", - "disabled": "Deaktiveret", - "documentation": "Dokumentation", - "hacs_is_disabled": "HACS er deaktiveret", - "installed": "installeret", - "integration": "Integration", - "integrations": "Integrationer", - "manage": "administrer", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python-script", - "python_scripts": "Python-scripts", - "repositories": "Repositories", - "settings": "indstillinger", - "theme": "Tema", - "themes": "Temaer", - "version": "Version" - }, - "config": { - "abort": { - "single_instance_allowed": "Kun en enkelt konfiguration af HACS er tilladt." - }, - "error": { - "auth": "Personlig adgangstoken er ikke korrekt." - }, - "step": { - "user": { - "data": { - "appdaemon": "Aktiver opdagelse & sporing af AppDaemon-apps", - "python_script": "Aktivér opdagelse og sporing af python_scripts", - "sidepanel_icon": "Sidepanelikon", - "sidepanel_title": "Sidepanelets titel", - "theme": "Aktivér opdagelse og sporing af temaer", - "token": "GitHub personlig adgangstoken" - }, - "description": "Hvis du har brug for hjælp til konfigurationen, så kig her: https:\/\/hacs.xyz\/docs\/configuration\/start" - } - } - }, - "confirm": { - "add_to_lovelace": "Er du sikker på, at du vil tilføje dette til dine Lovelace-ressourcer?", - "bg_task": "Handlingen er deaktiveret, mens baggrundsopgaver kører.", - "cancel": "Annuller", - "continue": "Er du sikker på, at du vil fortsætte?", - "delete": "Er du sikker på, at du vil slette '{Item}'?", - "delete_installed": "'{item}' er installeret. Du skal afinstallere det, før du kan slette det.", - "exist": "{item} findes allerede", - "generic": "Er du sikker?", - "home_assistant_is_restarting": "Vent venligst - Home Assistant genstarter nu.", - "home_assistant_version_not_correct": "Du kører Home Assistant version '{haversion}', men dette repository kræver som minimum version '{minversion}'.", - "no": "Nej", - "no_upgrades": "Der er ingen opdateringer tilgængelig", - "ok": "OK", - "overwrite": "Dette vil overskrive den.", - "reload_data": "Dette genindlæser data fra alle repositories, som HACS kender til. Dette vil tage nogen tid at fuldføre.", - "restart_home_assistant": "Er du sikker på, at du vil genstarte Home Assistant?", - "uninstall": "Er du sikker på, at du vil afinstallere '{Item}'?", - "upgrade_all": "Dette vil opdatere alle repositories. Sørg for at du har læst udgivelsesnoterne for dem alle, inden du fortsætter.", - "yes": "Ja" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Aktiver opdagelse og sporing af AppDaemon-apps", - "country": "Filtrer med landekode.", - "debug": "Aktiver debug.", - "experimental": "Aktivér eksperimentelle funktioner", - "not_in_use": "Ikke i brug med YAML", - "release_limit": "Antal udgivelser, der skal vises.", - "sidepanel_icon": "Sidepanelikon", - "sidepanel_title": "Sidepanelets titel" - } - } - } - }, - "repository_banner": { - "config_flow": "Denne integration understøtter config_flow. Det betyder, at du nu kan gå til integrationssektionen i din brugerflade for at konfigurere den.", - "config_flow_title": "Brugerfladekonfiguration understøttet", - "integration_not_loaded": "Denne integration er ikke indlæst i Home Assistant.", - "no_restart_required": "Ingen genstart påkrævet", - "not_loaded": "Ikke indlæst", - "plugin_not_loaded": "Dette plugin er ikke føjet til dine Lovelace-ressourcer.", - "restart": "Du skal genstarte Home Assistant.", - "restart_pending": "Afventer genstart" - }, - "repository": { - "add_to_lovelace": "Tilføj til Lovelace", - "authors": "Forfattere", - "available": "Tilgængelig", - "back_to": "Tilbage til", - "changelog": "Udgivelsesnoter", - "downloads": "Downloads", - "flag_this": "Marker denne", - "frontend_version": "Frontend-version", - "github_stars": "GitHub-stjerner", - "goto_integrations": "Gå til integrationer", - "hide": "Skjul", - "hide_beta": "Skjul beta", - "install": "Installer", - "installed": "Installeret", - "lovelace_copy_example": "Kopiér eksemplet til din Udklipsholder", - "lovelace_instruction": "Tilføj dette til din lovelace-konfiguration", - "lovelace_no_js_type": "Kunne ikke afgøre typen af dette plugin, tjek venligst repository.", - "newest": "nyeste", - "note_appdaemon": "Du skal stadig føje den til filen 'apps.yaml'", - "note_installed": "Når det er installeret, vil dette være placeret i", - "note_integration": "du skal stadig føje den til filen 'configuration.yaml'", - "note_plugin": "du skal stadig tilføje det til din lovelace-konfiguration ('ui-lovelace.yaml' eller Tekstbaseret redigering)", - "open_issue": "Opret issue", - "open_plugin": "Åbn plugin", - "reinstall": "Geninstaller", - "repository": "Repository", - "restart_home_assistant": "Genstart Home Assistant", - "show_beta": "Vis beta", - "uninstall": "Afinstaller", - "update_information": "Opdater oplysninger", - "upgrade": "Opdater" - }, - "settings": { - "add_custom_repository": "TILFØJ ET BRUGERDEFINERET REPOSITORY", - "adding_new_repo": "Tilføjer nyt repository '{repo}'", - "adding_new_repo_category": "Med kategorien '{category}'.", - "bg_task_custom": "Brugerdefinerede repositories er skjult, mens opgaver i baggrunden kører.", - "category": "Kategori", - "compact_mode": "Kompakt tilstand", - "custom_repositories": "BRUGERDEFINEREDE REPOSITORIES", - "delete": "Slet", - "display": "Visning", - "grid": "Gitter", - "hacs_repo": "HACS-repo", - "hidden_repositories": "Skjulte repositories", - "missing_category": "Du skal vælge en kategori", - "open_repository": "Åbn repository", - "reload_data": "Genindlæs data", - "reload_window": "Genindlæs vindue", - "repository_configuration": "Konfiguration af repository", - "save": "Gem", - "table": "Tabel", - "table_view": "Tabelvisning", - "unhide": "Vis", - "upgrade_all": "Opdater alle" - }, - "store": { - "ascending": "stigende", - "clear_new": "Marker alle som set", - "descending": "faldende", - "last_updated": "Sidst opdateret", - "name": "Navn", - "new_repositories": "Nye repositories", - "pending_upgrades": "Afventende opdateringer", - "placeholder_search": "Indtast en søgeterm...", - "sort": "sorter", - "stars": "Stjerner", - "status": "Status" - }, - "time": { - "ago": "siden", - "day": "dag", - "days": "dage", - "hour": "time", - "hours": "timer", - "minute": "minut", - "minutes": "minutter", - "month": "måned", - "months": "måneder", - "one": "Et", - "one_day_ago": "en dag siden", - "one_hour_ago": "en time siden", - "one_minute_ago": "et minut siden", - "one_month_ago": "en måned siden", - "one_second_ago": "et sekund siden", - "one_year_ago": "et år siden", - "second": "sekund", - "seconds": "sekunder", - "x_days_ago": "{x} dage siden", - "x_hours_ago": "{x} timer siden", - "x_minutes_ago": "{x} minutter siden", - "x_months_ago": "{x} måneder siden", - "x_seconds_ago": "{x} sekunder siden", - "x_years_ago": "{x} år siden", - "year": "år", - "years": "år" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/de.json b/custom_components/hacs/.translations/de.json deleted file mode 100644 index f96ac00..0000000 --- a/custom_components/hacs/.translations/de.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "common": { - "about": "Über", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Apps", - "background_task": "Hintergrundprozess läuft. Diese Seite lädt neu, sobald dieser fertig ist.", - "check_log_file": "Überprüfe die Logdatei für weitere Informationen.", - "continue": "Fortfahren", - "disabled": "Deaktiviert", - "documentation": "Dokumentation", - "hacs_is_disabled": "HACS ist deaktiviert", - "installed": "Installiert", - "integration": "Integration", - "integrations": "Integrationen", - "manage": "verwalten", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Apps", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python Skript", - "python_scripts": "Python Skripte", - "repositories": "Repositories", - "settings": "Einstellungen", - "theme": "Theme", - "themes": "Themes", - "version": "Version" - }, - "config": { - "abort": { - "single_instance_allowed": "Es ist nur eine einzelne HACS-Instanz erlaubt." - }, - "error": { - "auth": "Persönlicher Zugriffstoken ist falsch." - }, - "step": { - "user": { - "data": { - "appdaemon": "AppDaemon App-Entdeckung & Nachverfolgung aktivieren", - "netdaemon": "NetDaemon App-Entdeckung & Nachverfolgung aktivieren", - "python_script": "Python Script-Entdeckung & Nachverfolgung aktivieren", - "sidepanel_icon": "Sidepanel Symbol", - "sidepanel_title": "Sidepanel Titel", - "theme": "Theme-Entdeckung & Nachverfolgung aktivieren", - "token": "Persönlicher GitHub Zugriffstoken" - }, - "description": "Wenn du Hilfe mit den Einstellungen brauchst, kannst du hier nachsehen: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Möchtest du dies wirklich zu deinen Lovelace-Ressourcen hinzufügen?", - "bg_task": "Die Aktion ist deaktiviert, während Hintergrundprozesse ausgeführt werden.", - "cancel": "Abbrechen", - "continue": "Bist du dir sicher, dass du fortfahren möchtest?", - "delete": "Möchtest du '{item}' wirklich löschen?", - "delete_installed": "'{item}' ist installiert. Du musst es deinstallieren, bevor du es löschen kannst.", - "exist": "{item} existiert bereits", - "generic": "Bist du dir sicher?", - "home_assistant_is_restarting": "Bitte warte, Home Assistant wird jetzt neu gestartet.", - "home_assistant_version_not_correct": "Du benutzt die Home Assistant-Version '{haversion}', für dieses Repository muss jedoch die Mindestversion '{minversion}' installiert sein.", - "no": "Nein", - "no_upgrades": "Keine Upgrades ausstehend", - "ok": "OK", - "overwrite": "Dadurch wird die Datei überschrieben.", - "reload_data": "Hierdurch werden die Daten aller Repositories die HACS kennt neu geladen. Dies wird einige Zeit in Anspruch nehmen.", - "restart_home_assistant": "Bist du dir sicher, dass du Home Assistant neu starten möchtest?", - "uninstall": "Möchtest du '{item}' wirklich deinstallieren?", - "upgrade_all": "Hierdurch werden all diese Repositories aktualisiert. Stelle sicher, dass du die Versionshinweise vorher gelesen hast.", - "yes": "Ja" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "AppDaemon App-Entdeckung & Nachverfolgung aktivieren", - "country": "Nach Ländercode filtern.", - "debug": "Debug aktivieren.", - "experimental": "Experimentelle Funktionen aktivieren", - "netdaemon": "NetDaemon App-Entdeckung & Nachverfolgung aktivieren", - "not_in_use": "Nicht in Verwendung mit YAML", - "release_limit": "Anzahl anzuzeigender Releases.", - "sidepanel_icon": "Sidepanel Symbol", - "sidepanel_title": "Sidepanel Titel" - } - } - } - }, - "repository_banner": { - "config_flow": "Diese Integration unterstützt config_flow. Das bedeutet, dass du jetzt in den Integrationsbereich deiner Benutzeroberfläche wechseln kannst, um es zu konfigurieren.", - "config_flow_title": "UI-Konfiguration unterstützt", - "integration_not_loaded": "Diese Integration ist in Home Assistant nicht geladen.", - "no_restart_required": "Kein Neustart erforderlich", - "not_loaded": "Nicht geladen", - "plugin_not_loaded": "Dieses Plugin wurde deinen Lovelace-Ressourcen nicht hinzugefügt.", - "restart": "Du musst Home Assistant neu starten.", - "restart_pending": "Neustart ausstehend" - }, - "repository": { - "add_to_lovelace": "Zu Lovelace hinzufügen", - "authors": "Autoren", - "available": "Verfügbar", - "back_to": "Zurück zu", - "changelog": "Änderungsprotokoll", - "downloads": "Downloads", - "flag_this": "Melden", - "frontend_version": "Frontend Version", - "github_stars": "GitHub Sterne", - "goto_integrations": "Gehe zu Integrationen", - "hide": "Verstecken", - "hide_beta": "Beta verstecken", - "install": "Installieren", - "installed": "Installiert", - "lovelace_copy_example": "Beispiel in die Zwischenablage kopieren", - "lovelace_instruction": "Zum Hinzufügen zu deinen Lovelace-Einstellungen, benutze Folgendes", - "lovelace_no_js_type": "Konnte die Art dieses Plugins nicht erkennen. Prüfe das Repository.", - "newest": "neueste", - "note_appdaemon": "du musst es dann noch in die Datei 'apps.yaml' hinzufügen", - "note_installed": "Wird installiert nach", - "note_integration": "du musst es dann noch in die Datei 'configuration.yaml' hinzufügen", - "note_plugin": "du musst es dann noch in deine Lovelace-Einstellungen ('ui-lovelace.yaml' oder im Raw-Konfigurationseditor) hinzufügen", - "note_plugin_post_107": "Du musst es noch zu deiner Lovelace-Konfiguration hinzufügen ('configuration.yaml' oder der Ressourceneditor '\/config\/lovelace\/resources')", - "open_issue": "Problem melden", - "open_plugin": "Plugin öffnen", - "reinstall": "Neu installieren", - "repository": "Repository", - "restart_home_assistant": "Home Assistant neu starten", - "show_beta": "Beta anzeigen", - "uninstall": "Deinstallieren", - "update_information": "Aktualisierungsinformationen", - "upgrade": "Aktualisieren" - }, - "settings": { - "add_custom_repository": "BENUTZERDEFINIERTES REPOSITORY HINZUFÜGEN", - "adding_new_repo": "Hinzufügen eines neuen Repository '{repo}'", - "adding_new_repo_category": "Mit der Kategorie '{category}'.", - "bg_task_custom": "Custom Repositorys werden ausgeblendet, während Hintergrundaufgaben ausgeführt werden.", - "category": "Kategorie", - "compact_mode": "Kompakter Modus", - "custom_repositories": "BENUTZERDEFINIERTE REPOSITORIES", - "delete": "Löschen", - "display": "Anzeige", - "grid": "Gitter", - "hacs_repo": "HACS repo", - "hidden_repositories": "versteckte Repositories", - "missing_category": "Du musst eine Kategorie auswählen.", - "open_repository": "Repository öffnen", - "reload_data": "Daten neu laden", - "reload_window": "Fenster neu laden", - "repository_configuration": "Repository Konfiguration", - "save": "Speichern", - "table": "Tabelle", - "table_view": "Tabellenansicht", - "unhide": "einblenden", - "upgrade_all": "Alles aktualisieren" - }, - "store": { - "ascending": "Aufsteigend", - "clear_new": "Alle neuen Repositories als gesehen markieren", - "descending": "Absteigend", - "last_updated": "Zuletzt aktualisiert", - "name": "Name", - "new_repositories": "Neue Repositories", - "pending_upgrades": "Ausstehende Upgrades", - "placeholder_search": "Suchbegriff eingeben…", - "sort": "Sortieren", - "stars": "Sterne", - "status": "Status" - }, - "time": { - "ago": "vor", - "day": "Tag", - "days": "Tage", - "hour": "Stunde", - "hours": "Stunden", - "minute": "Minute", - "minutes": "Minuten", - "month": "Monat", - "months": "Monate", - "one": "Eins", - "one_day_ago": "vor einem Tag", - "one_hour_ago": "vor einer Stunde", - "one_minute_ago": "vor einer Minute", - "one_month_ago": "vor einem Monat", - "one_second_ago": "vor einer Sekunde", - "one_year_ago": "vor einem Jahr", - "second": "Sekunde", - "seconds": "Sekunden", - "x_days_ago": "vor {x} Tagen", - "x_hours_ago": "vor {x} Stunden", - "x_minutes_ago": "vor {x} Minuten", - "x_months_ago": "vor {x} Monaten", - "x_seconds_ago": "vor {x} Sekunden", - "x_years_ago": "vor {x} Jahren", - "year": "Jahr", - "years": "Jahre" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/el.json b/custom_components/hacs/.translations/el.json deleted file mode 100644 index 0d446ad..0000000 --- a/custom_components/hacs/.translations/el.json +++ /dev/null @@ -1,186 +0,0 @@ -{ - "common": { - "about": "Σχετικά με", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Apps", - "background_task": "Τρέχει μια διεργασία στο παρασκήνιο, η σελίδα θα ανανεωθεί μόλις αυτό ολοκληρωθεί.", - "check_log_file": "Ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες.", - "continue": "Να συνεχίσει", - "disabled": "Απενεργοποιημένο", - "documentation": "Τεκμηρίωση", - "hacs_is_disabled": "Το HACS είναι απενεργοποιημένο", - "installed": "εγκατεστημένο", - "integration": "Ενσωμάτωση", - "integrations": "Ενσωματωμένα", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Apps", - "plugin": "Πρόσθετο", - "plugins": "Πρόσθετα", - "python_script": "Πρόγραμμα Python", - "python_scripts": "Προγράμματα Python", - "repositories": "Αποθετήρια", - "settings": "ρυθμίσεις", - "theme": "Θέμα", - "themes": "Θέματα", - "version": "Έκδοση" - }, - "config": { - "abort": { - "single_instance_allowed": "Μονάχα μία ρύθμιση των παραμέτρων του HACS επιτρέπεται." - }, - "error": { - "auth": "Το διακριτικό πρόσβασης δεν είναι σωστό." - }, - "step": { - "user": { - "data": { - "appdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το AppDaemon", - "netdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το NetDaemon", - "python_script": "Ενεργοποίηση εύρεσης & παρακολούθησης για τα python_scripts", - "sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ", - "sidepanel_title": "Τίτλος πλαϊνού πάνελ", - "theme": "Ενεργοποίηση εύρεσης & παρακολούθησης για τα θεμάτων", - "token": "Διακριτικό πρόσβασης του GitHub" - }, - "description": "Εάν χρειαστείτε βοήθεια με τις ρυθμίσεις ανατρέξτε εδώ: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Είστε βέβαιοι ότι θέλετε να προσθέσετε αυτό στους πόρους του Lovelace;", - "bg_task": "Η ενέργεια είναι απενεργοποιημένη όσο εκτελούνται εργασίες στο παρασκήνιο", - "cancel": "Ακύρωση", - "continue": "Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", - "delete": "Είστε σίγουροι ότι θέλετε να διαγράψετε το '{item}';", - "delete_installed": "Το '{item}' είναι εγκατεστημένο, πρέπει να το απεγκαταστήσετε πριν να το διαγράψετε.", - "exist": "{item} υπάρχει ήδη", - "generic": "Είστε βέβαιοι;", - "home_assistant_is_restarting": "Περιμένετε, το Home Assistant επανεκκινείται τώρα.", - "no": "Οχι", - "no_upgrades": "Δεν υπάρχουν αναβαθμίσεις σε εκκρεμότητα", - "ok": "Εντάξει", - "overwrite": "Αυτό θα το αντικαταστήσει.", - "restart_home_assistant": "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση του Home Assistant;", - "uninstall": "Είστε βέβαιοι ότι θέλετε να απεγκαταστήσετε το '{item}';", - "upgrade_all": "Αυτό θα αναβαθμίσει όλα αυτά τα αποθετήρια, βεβαιωθείτε ότι έχετε διαβάσει τις σημειώσεις έκδοσης για όλα πριν προχωρήσετε.", - "yes": "Ναι" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το AppDaemon", - "country": "Κριτήριο με βάση τον κωδικό χώρας.", - "debug": "Ενεργοποίηση εντοπισμού σφαλμάτων.", - "experimental": "Ενεργοποίση πειραματικών λειτουργιών", - "netdaemon": "Ενεργοποίηση εύρεσης & παρακολούθησης για το NetDaemon", - "release_limit": "Αριθμός εκδόσεων που να παραθέτονται.", - "sidepanel_icon": "Εικονίδιο πλαϊνού πάνελ", - "sidepanel_title": "Τίτλος πλαϊνού πάνελ" - } - } - } - }, - "repository_banner": { - "integration_not_loaded": "Αυτή η ενσωμάτωση δεν φορτώθηκε στο Home Assistant.", - "no_restart_required": "Δεν απαιτείται επανεκκίνηση", - "not_loaded": "Δεν έχει φορτωθεί", - "plugin_not_loaded": "Αυτό το πρόσθετο δεν προστέθηκε στους πόρους του Lovelace.", - "restart": "Πρέπει να κάνετε επανεκκίνηση του Home Assistant.", - "restart_pending": "Επανεκκίνηση σε εκκρεμότητα" - }, - "repository": { - "add_to_lovelace": "Προσθήκη στο Lovelace", - "authors": "Συγγραφείς", - "available": "Διαθέσιμο", - "back_to": "Πίσω στο", - "changelog": "Σημειώσεις των αλλαγών", - "downloads": "Λήψεις", - "flag_this": "Σημείωσε αυτό", - "frontend_version": "Έκδοση Frontend", - "hide": "Απόκρυψη", - "hide_beta": "Απόκριση του beta", - "install": "Εγκατάσταση", - "installed": "Εγκατεστημένο", - "lovelace_copy_example": "Αντίγραψε το παράδειγμα στο πρόχειρο", - "lovelace_instruction": "Όταν το προσθέσετε στις ρυθμίσεις του lovelace χρησιμοποιήστε τούτο", - "lovelace_no_js_type": "Δεν καταφέραμε να προσδιορίσουμε τον τύπο αυτού του προσθέτου, ελέξτε το αποθετήριο.", - "newest": "νεότερο", - "note_appdaemon": "εξακολουθεί να χρειάζεται να το προσθέσετε στο αρχείο 'apps.yaml'", - "note_installed": "Όταν εγκατασταθεί, θα προστεθεί στο", - "note_integration": "εξακολουθεί να χρειάζεται να το προσθέσετε στο αρχείο 'configuration.yaml'", - "note_plugin": "εξακολουθεί να χρειάζετε να το προσθέσετε στις ρυθμίσεις του lovelace ('ui-lovelace.yaml' ή μέσω του γραφικού επεξεργαστή των ρυθμίσεων)", - "open_issue": "Εκκρεμόν ζήτημα", - "open_plugin": "Ανοιχτό πρόσθετο", - "reinstall": "Επανεγκατάσταση", - "repository": "Αποθετήριο", - "restart_home_assistant": "Επανεκκίνηση του Home Assistant", - "show_beta": "Εμφάνιση του beta", - "uninstall": "Απεγκατάσταση", - "update_information": "Ενημέρωση πληροφοριών", - "upgrade": "Ενημέρωση" - }, - "settings": { - "add_custom_repository": "ΠΡΟΣΘΕΣΤΕ ΕΝΑ ΕΙΔΙΚΟ ΑΠΟΘΕΤΗΡΙΟ", - "adding_new_repo_category": "Με κατηγορία '{category}'.", - "category": "Κατηγορία", - "compact_mode": "Συμπαγής λειτουργία", - "custom_repositories": "ΕΙΔΙΚΑ ΑΠΟΘΕΤΗΡΙΑ", - "delete": "Διαγραφή", - "display": "Εμφάνιση", - "grid": "Πλέγμα", - "hacs_repo": "Αποθετήριο του HACS", - "hidden_repositories": "κρυφά αποθετήρια", - "missing_category": "Πρέπει να επιλέξετε μια κατηγορία", - "open_repository": "Ανοίξτε το αποθετήριο", - "reload_data": "Επαναφόρτωση δεδομένων", - "reload_window": "Επαναφόρτωση του παραθύρου", - "save": "Αποθήκευση", - "table": "Πίνακας", - "table_view": "Προβολή πίνακα", - "upgrade_all": "Αναβάθμιση όλων" - }, - "store": { - "ascending": "αύξουσα", - "clear_new": "Απαλοιφή όλων των νέων αποθετηρίων", - "descending": "φθίνουσα", - "last_updated": "Τελευταία ενημέρωση", - "name": "Ονομα", - "new_repositories": "Νέα αποθετήρια", - "pending_upgrades": "Εκκρεμείς αναβαθμίσεις", - "placeholder_search": "Παρακαλώ πληκτρολογήστε έναν όρο προς αναζήτηση...", - "sort": "είδος", - "stars": "Αστέρια", - "status": "Κατάσταση" - }, - "time": { - "ago": "πριν", - "day": "ημέρα", - "days": "ημέρες", - "hour": "ώρα", - "hours": "ώρες", - "minute": "λεπτό", - "minutes": "λεπτά", - "month": "μήνας", - "months": "μήνες", - "one": "Ένα", - "one_day_ago": "πριν από μία ημέρα", - "one_hour_ago": "πριν από μια ώρα", - "one_minute_ago": "πριν από ένα λεπτό", - "one_month_ago": "πριν από ένα μήνα", - "one_second_ago": "πριν από ένα δευτερόλεπτο", - "one_year_ago": "πριν από ένα χρόνο", - "second": "δευτερόλεπτο", - "seconds": "δευτερόλεπτα", - "x_days_ago": "{x} ημέρες πριν", - "x_hours_ago": "{x} ώρες πριν", - "x_minutes_ago": "{x} λεπτά πριν", - "x_months_ago": "{x} μήνες πριν", - "x_seconds_ago": "{x} δευτερόλεπτα πριν", - "x_years_ago": "{x} χρόνια πριν", - "year": "έτος", - "years": "χρόνια" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/en.json b/custom_components/hacs/.translations/en.json deleted file mode 100644 index 387c89f..0000000 --- a/custom_components/hacs/.translations/en.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "common": { - "about": "About", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Apps", - "background_task": "Background task running, this page will reload when it's done.", - "check_log_file": "Check your log file for more details.", - "continue": "Continue", - "disabled": "Disabled", - "documentation": "Documentation", - "hacs_is_disabled": "HACS is disabled", - "installed": "installed", - "integration": "Integration", - "integrations": "Integrations", - "manage": "manage", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Apps", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python Script", - "python_scripts": "Python Scripts", - "repositories": "Repositories", - "settings": "settings", - "theme": "Theme", - "themes": "Themes", - "version": "Version" - }, - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of HACS is allowed." - }, - "error": { - "auth": "Personal Access Token is not correct." - }, - "step": { - "user": { - "data": { - "appdaemon": "Enable AppDaemon apps discovery & tracking", - "netdaemon": "Enable NetDaemon apps discovery & tracking", - "python_script": "Enable python_scripts discovery & tracking", - "sidepanel_icon": "Side panel icon", - "sidepanel_title": "Side panel title", - "theme": "Enable Themes discovery & tracking", - "token": "GitHub Personal Access Token" - }, - "description": "If you need help with the configuration have a look here: https:\/\/hacs.xyz\/docs\/configuration\/start\/", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Are you sure you want to add this to your Lovelace resources?", - "bg_task": "Action is disabled while background tasks is running.", - "cancel": "Cancel", - "continue": "Are you sure you want to continue?", - "delete": "Are you sure you want to delete \"{item}\"?", - "delete_installed": "'{item}' is installed, you need to uninstall it before you can delete it.", - "exist": "{item} already exists", - "generic": "Are you sure?", - "home_assistant_is_restarting": "Hold on, Home Assistant is now restarting.", - "home_assistant_version_not_correct": "You are running Home Assistant version '{haversion}', but this repository requires minimum version '{minversion}' to be installed.", - "no": "No", - "no_upgrades": "No upgrades pending", - "ok": "OK", - "overwrite": "Doing this will overwrite it.", - "reload_data": "This reloads the data of all repositories HACS knows about, this will take some time to finish.", - "restart_home_assistant": "Are you sure you want to restart Home Assistant?", - "uninstall": "Are you sure you want to uninstall \"{item}\"?", - "upgrade_all": "This will upgrade all of these repositories, make sure that you have read the release notes for all of them before proceeding.", - "yes": "Yes" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Enable AppDaemon apps discovery & tracking", - "country": "Filter with country code.", - "debug": "Enable debug.", - "experimental": "Enable experimental features", - "netdaemon": "Enable NetDaemon apps discovery & tracking", - "not_in_use": "Not in use with YAML", - "release_limit": "Number of releases to show.", - "sidepanel_icon": "Side panel icon", - "sidepanel_title": "Side panel title" - } - } - } - }, - "repository_banner": { - "config_flow": "This integration supports config_flow, that means that you now can go to the integration section of your UI to configure it.", - "config_flow_title": "UI Configuration supported", - "integration_not_loaded": "This integration is not loaded in Home Assistant.", - "no_restart_required": "No restart required", - "not_loaded": "Not loaded", - "plugin_not_loaded": "This plugin is not added to your Lovelace resources.", - "restart": "You need to restart Home Assistant.", - "restart_pending": "Restart pending" - }, - "repository": { - "add_to_lovelace": "Add to Lovelace", - "authors": "Authors", - "available": "Available", - "back_to": "Back to", - "changelog": "Change log", - "downloads": "Downloads", - "flag_this": "Flag this", - "frontend_version": "Frontend version", - "github_stars": "GitHub stars", - "goto_integrations": "Go to integrations", - "hide": "Hide", - "hide_beta": "Hide beta", - "install": "Install", - "installed": "Installed", - "lovelace_copy_example": "Copy the example to your clipboard", - "lovelace_instruction": "When you add this to your lovelace configuration use this", - "lovelace_no_js_type": "Could not determine the type of this plugin, check the repository.", - "newest": "newest", - "note_appdaemon": "you still need to add it to your 'apps.yaml' file", - "note_installed": "When installed, this will be located in", - "note_integration": "you still need to add it to your 'configuration.yaml' file", - "note_plugin": "you still need to add it to your lovelace configuration ('ui-lovelace.yaml' or the raw UI config editor)", - "note_plugin_post_107": "you still need to add it to your lovelace configuration ('configuration.yaml' or the resource editor '\/config\/lovelace\/resources')", - "open_issue": "Open issue", - "open_plugin": "Open plugin", - "reinstall": "Reinstall", - "repository": "Repository", - "restart_home_assistant": "Restart Home Assistant", - "show_beta": "Show beta", - "uninstall": "Uninstall", - "update_information": "Update information", - "upgrade": "Update" - }, - "settings": { - "add_custom_repository": "ADD CUSTOM REPOSITORY", - "adding_new_repo": "Adding new repository '{repo}'", - "adding_new_repo_category": "With category '{category}'.", - "bg_task_custom": "Custom repositories are hidden while background tasks is running.", - "category": "Category", - "compact_mode": "Compact mode", - "custom_repositories": "CUSTOM REPOSITORIES", - "delete": "Delete", - "display": "Display", - "grid": "Grid", - "hacs_repo": "HACS repo", - "hidden_repositories": "hidden repositories", - "missing_category": "You need to select a category", - "open_repository": "Open repository", - "reload_data": "Reload data", - "reload_window": "Reload window", - "repository_configuration": "Repository configuration", - "save": "Save", - "table": "Table", - "table_view": "Table view", - "unhide": "unhide", - "upgrade_all": "Upgrade all" - }, - "store": { - "ascending": "ascending", - "clear_new": "Clear all new repositories", - "descending": "descending", - "last_updated": "Last updated", - "name": "Name", - "new_repositories": "New Repositories", - "pending_upgrades": "Pending upgrades", - "placeholder_search": "Please enter a search term...", - "sort": "sort", - "stars": "Stars", - "status": "Status" - }, - "time": { - "ago": "ago", - "day": "day", - "days": "days", - "hour": "hour", - "hours": "hours", - "minute": "minute", - "minutes": "minutes", - "month": "month", - "months": "months", - "one": "One", - "one_day_ago": "one day ago", - "one_hour_ago": "one hour ago", - "one_minute_ago": "one minute ago", - "one_month_ago": "one month ago", - "one_second_ago": "one second ago", - "one_year_ago": "one year ago", - "second": "second", - "seconds": "seconds", - "x_days_ago": "{x} days ago", - "x_hours_ago": "{x} hours ago", - "x_minutes_ago": "{x} minutes ago", - "x_months_ago": "{x} months ago", - "x_seconds_ago": "{x} seconds ago", - "x_years_ago": "{x} years ago", - "year": "year", - "years": "years" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/es.json b/custom_components/hacs/.translations/es.json deleted file mode 100644 index ccc56f4..0000000 --- a/custom_components/hacs/.translations/es.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "common": { - "about": "Acerca de", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Apps", - "background_task": "Ejecutando tareas en segundo plano. Se refrescará automaticamente esta página al finalizar.", - "check_log_file": "Compruebe el archivo de registro para obtener más detalles.", - "continue": "Continuar", - "disabled": "Deshabilitado", - "documentation": "Documentación", - "hacs_is_disabled": "HACS está deshabilitado", - "installed": "instalado", - "integration": "Integración", - "integrations": "Integraciones", - "manage": "Administrar", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Apps", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python Script", - "python_scripts": "Python Scripts", - "repositories": "Repositorios", - "settings": "configuraciones", - "theme": "Tema", - "themes": "Temas", - "version": "Versión" - }, - "config": { - "abort": { - "single_instance_allowed": "Sólo se permite una única configuración de HACS." - }, - "error": { - "auth": "El token de acceso personal no es correcto." - }, - "step": { - "user": { - "data": { - "appdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de AppDaemon", - "netdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de NetDaemon", - "python_script": "Habilitar el descubrimiento y seguimiento en python_scripts", - "sidepanel_icon": "Ícono del panel lateral", - "sidepanel_title": "Título del panel lateral", - "theme": "Habilitar el descubrimiento y seguimiento de temas", - "token": "Token de acceso personal de GitHub" - }, - "description": "Si necesitas ayuda con la configuración, visita la siguiente pagina: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "¿Está seguro de que desea agregar esto a sus recursos de Lovelace?", - "bg_task": "La acción está deshabilitada mientras se ejecutan tareas en segundo plano.", - "cancel": "Cancelar", - "continue": "Estás seguro de que quieres continuar?", - "delete": "¿Seguro que quieres eliminar '{item}'?", - "delete_installed": "'{item}' está instalado, debe desinstalarlo antes de poder eliminarlo.", - "exist": "{item} ya existe", - "generic": "¿Estás seguro?", - "home_assistant_is_restarting": "Espera, Home Assistant se está reiniciando.", - "home_assistant_version_not_correct": "Está ejecutando la versión '{haversion}' de Home Assistant, pero este repositorio requiere la instalación de la versión '{minversion}' mínima.", - "no": "No", - "no_upgrades": "No hay actualizaciones pendientes", - "ok": "OK", - "overwrite": "Si lo hace, se sobrescribirá.", - "reload_data": "Esto recarga los datos de todos los repositorios que HACS conoce, esto tardará algún tiempo en finalizar.", - "restart_home_assistant": "¿Está seguro de que desea reiniciar Home Assistant?", - "uninstall": "¿Está seguro de que desea desinstalar '{item}'?", - "upgrade_all": "Esto actualizará todos estos repositorios, asegúrese de que ha leído las notas de la versión de todos ellos antes de continuar.", - "yes": "Si" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de AppDaemon", - "country": "Filtrar por el código de país.", - "debug": "Habilitar depuración.", - "experimental": "Habilitar funciones experimentales", - "netdaemon": "Habilitar el descubrimiento y seguimiento de las aplicaciones de NetDaemon", - "not_in_use": "No usarse con YAML", - "release_limit": "Número de versiones a mostrar.", - "sidepanel_icon": "Icono del panel lateral", - "sidepanel_title": "Título del panel lateral" - } - } - } - }, - "repository_banner": { - "config_flow": "Esta integración soporta config_flow, lo que significa que ahora puede ir a la sección de integración de su UI para configurarlo.", - "config_flow_title": "Configuración de UI soportada", - "integration_not_loaded": "Esta integración no se carga en Home Assistant.", - "no_restart_required": "No es necesario reiniciar", - "not_loaded": "No está cargado", - "plugin_not_loaded": "Este plugin no se añade a sus recursos de Lovelace.", - "restart": "Es necesario reiniciar Home Assistant.", - "restart_pending": "Reinicio pendiente" - }, - "repository": { - "add_to_lovelace": "Añadir a Lovelace", - "authors": "Autores", - "available": "Disponible", - "back_to": "Volver a", - "changelog": "Registro de cambios", - "downloads": "Descargas", - "flag_this": "Marcar esto", - "frontend_version": "Versión del frontend", - "github_stars": "Estrellas de GitHub", - "goto_integrations": "Ir a integraciones", - "hide": "Ocultar", - "hide_beta": "Ocultar beta", - "install": "Instalar", - "installed": "Instalado", - "lovelace_copy_example": "Copiar el ejemplo al clipboard", - "lovelace_instruction": "Agregue lo siguiente en su configuración de lovelace", - "lovelace_no_js_type": "No se pudo determinar el tipo de plugin, revise el repositorio.", - "newest": "más nuevo", - "note_appdaemon": "deberá agregar esto a su archivo 'apps.yaml'", - "note_installed": "Al instalarse se encontrará en", - "note_integration": "deberá agregar esto a su archivo 'configuration.yaml'", - "note_plugin": "deberá agregar esto a su configuración de lovelace ('ui-lovelace.yaml' o en el editor UI de lovelace)", - "open_issue": "Abrir issue", - "open_plugin": "Abrir plugin", - "reinstall": "Reinstalar", - "repository": "Repositorio", - "restart_home_assistant": "Reiniciar Home Assistant", - "show_beta": "Mostrar beta", - "uninstall": "Desinstalar", - "update_information": "Actualizar información", - "upgrade": "Actualizar" - }, - "settings": { - "add_custom_repository": "AGREGAR REPOSITORIO PERSONALIZADO", - "adding_new_repo": "Añadiendo un nuevo repositorio '{repo}'.", - "adding_new_repo_category": "Con la categoría '{category}'.", - "bg_task_custom": "Los repositorios personalizados están ocultos mientras se ejecutan las tareas en segundo plano.", - "category": "Categoría", - "compact_mode": "Modo compacto", - "custom_repositories": "REPOSITORIOS PERSONALIZADOS", - "delete": "Eliminar", - "display": "Mostrar", - "grid": "Cuadrícula", - "hacs_repo": "HACS repo", - "hidden_repositories": "repositorios ocultos", - "missing_category": "Es necesario seleccionar una categoría", - "open_repository": "Abrir repositorio", - "reload_data": "Recargar datos", - "reload_window": "Recargar ventana", - "repository_configuration": "Configuración del repositorio", - "save": "Grabar", - "table": "Tabla", - "table_view": "Vista de la tabla", - "unhide": "mostrar", - "upgrade_all": "Actualizar todo" - }, - "store": { - "ascending": "ascendente", - "clear_new": "Borrar todos los nuevos repositorios", - "descending": "descendente", - "last_updated": "Última actualización", - "name": "Nombre", - "new_repositories": "Nuevos Repositorios", - "pending_upgrades": "Actualizaciones pendientes", - "placeholder_search": "Por favor escriba una palabra clave de búsqueda...", - "sort": "ordenar", - "stars": "Estrellas", - "status": "Estado" - }, - "time": { - "ago": "hace", - "day": "dia", - "days": "dias", - "hour": "hora", - "hours": "horas", - "minute": "minuto", - "minutes": "minutos", - "month": "mes", - "months": "meses", - "one": "Uno", - "one_day_ago": "hace un día", - "one_hour_ago": "hace una hora", - "one_minute_ago": "hace un minuto", - "one_month_ago": "hace un mes", - "one_second_ago": "hace un segundo", - "one_year_ago": "hace un año", - "second": "segundo", - "seconds": "segundos", - "x_days_ago": "hace {x} dias", - "x_hours_ago": "hace {x} horas", - "x_minutes_ago": "hace {x} minutos", - "x_months_ago": "hace {x} meses", - "x_seconds_ago": "hace {x} segundos", - "x_years_ago": "hace {x} años", - "year": "año", - "years": "años" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/fr.json b/custom_components/hacs/.translations/fr.json deleted file mode 100644 index 2d2f487..0000000 --- a/custom_components/hacs/.translations/fr.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "common": { - "about": "À propos de", - "appdaemon": "AppDaemon", - "appdaemon_apps": "Applications AppDaemon", - "background_task": "Tache de fond en cours, cette page se rechargera une fois terminée", - "check_log_file": "Consultez votre fichier journal pour plus de détails.", - "continue": "Continuer", - "disabled": "Désactivé", - "documentation": "Documentation", - "hacs_is_disabled": "HACS est désactivé", - "installed": "installés", - "integration": "Intégration", - "integrations": "Intégrations", - "manage": "gérer", - "netdaemon": "NetDaemon", - "netdaemon_apps": "Applications NetDaemon", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Script Python", - "python_scripts": "Scripts Python", - "repositories": "Dépôts", - "settings": "paramètres", - "theme": "Thème", - "themes": "Thèmes", - "version": "Version" - }, - "config": { - "abort": { - "single_instance_allowed": "Une seule configuration de HACS est autorisée." - }, - "error": { - "auth": "Le jeton personnel d'accès est invalide." - }, - "step": { - "user": { - "data": { - "appdaemon": "Activer la découverte et le suivi des applications AppDaemon", - "netdaemon": "Activer la découverte et le suivi des applications NetDaemon", - "python_script": "Activer la découverte et le suivi des scripts python", - "sidepanel_icon": "Icône de la barre latérale", - "sidepanel_title": "Titre de la barre latérale", - "theme": "Activer la découverte et le suivi des thèmes", - "token": "Jeton d'accès personnel GitHub" - }, - "description": "Si vous avez besoin d'aide pour la configuration, jetez un œil ici: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Êtes-vous sûr de vouloir l'ajouter à vos ressources Lovelace ?", - "bg_task": "L'action est désactivée pendant l'exécution de tâches en arrière-plan.", - "cancel": "Annuler", - "continue": "Es-tu sur de vouloir continuer?", - "delete": "Êtes-vous sûr de vouloir supprimer '{item}'?", - "delete_installed": "'{item}' est installé, vous devez le désinstaller avant de pouvoir le supprimer.", - "exist": "{item} existe déjà", - "generic": "Êtes-vous sûr?", - "home_assistant_is_restarting": "Attendez, Home Assistant redémarre maintenant.", - "home_assistant_version_not_correct": "Vous utilisez la version '{haversion}' de Home Assistant, mais ce référentiel nécessite l'installation de la version minimale '{minversion}'.", - "no": "Non", - "no_upgrades": "Aucune mise à niveau en attente", - "ok": "OK", - "overwrite": "En faisant cela, cela l'écrasera.", - "reload_data": "Cela recharge les données de tous les dépôts dont HACS a connaissance, cela prendra un certain temps à terminer.", - "restart_home_assistant": "Voulez-vous vraiment redémarrer Home Assistant ?", - "uninstall": "Êtes-vous sûr de vouloir désinstaller '{item}'?", - "upgrade_all": "Cela mettra à niveau tous ces dépôts, assurez-vous d'avoir lu les notes de publication pour chacun d'entre eux avant de continuer.", - "yes": "Oui" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Activer la découverte et le suivi des applications AppDaemon", - "country": "Filtrer par code pays.", - "debug": "Activez le débogage.", - "experimental": "Activer les fonctionnalités expérimentales", - "netdaemon": "Activer la découverte et le suivi des applications NetDaemon", - "not_in_use": "Non utilisé avec YAML", - "release_limit": "Nombre de recensés à afficher.", - "sidepanel_icon": "Icône de la barre latérale", - "sidepanel_title": "Titre de la barre latérale" - } - } - } - }, - "repository_banner": { - "config_flow": "Cette intégration prend en charge config_flow, ce qui signifie que vous pouvez maintenant accéder à la section d'intégration de votre interface utilisateur pour le configurer.", - "config_flow_title": "Configuration de l'interface utilisateur prise en charge", - "integration_not_loaded": "Cette intégration n'est pas chargée dans Home Assistant.", - "no_restart_required": "Aucun redémarrage requis", - "not_loaded": "Non chargé", - "plugin_not_loaded": "Ce plugin n'est pas ajouté à vos ressources Lovelace.", - "restart": "Vous devez redémarrer Home Assistant.", - "restart_pending": "Redémarrage en attente" - }, - "repository": { - "add_to_lovelace": "Ajouter à Lovelace", - "authors": "Auteurs", - "available": "Disponible", - "back_to": "Retour", - "changelog": "Change log", - "downloads": "Téléchargements", - "flag_this": "Marquer", - "frontend_version": "Version de l'interface", - "github_stars": "Étoiles GitHub", - "goto_integrations": "Aller aux intégrations", - "hide": "Masquer", - "hide_beta": "Masquer les bêta", - "install": "Installer", - "installed": "Installé", - "lovelace_copy_example": "Copier cet exemple dans le presse-papier", - "lovelace_instruction": "Quand vous l'ajoutez à votre configuration lovelace, utilisez", - "lovelace_no_js_type": "Impossible de déterminer le type de plugin, veuillez vérifier le dépôt", - "newest": "nouveau", - "note_appdaemon": "vous devez toujours l'ajouter à votre fichier 'apps.yaml'", - "note_installed": "Une fois installé, il se trouvera dans", - "note_integration": "Vous devez toujours l'ajouter à votre fichier 'configuration.yaml'", - "note_plugin": "Vous devez toujours l'ajouter à votre configuration lovelace ('ui-lovelace.yaml' ou l'éditeur de configuration de l'interface)", - "open_issue": "Ouvrir un ticket", - "open_plugin": "Ouvrir le plugin", - "reinstall": "Réinstaller", - "repository": "Dépôt", - "restart_home_assistant": "Redémarrer Home Assistant", - "show_beta": "Afficher les bêta", - "uninstall": "Désinstaller", - "update_information": "Mettre à jour les informations", - "upgrade": "Mettre à jour" - }, - "settings": { - "add_custom_repository": "AJOUTER UN DÉPÔT PERSONNALISÉ", - "adding_new_repo": "Ajout d'un nouveau dépôt '{repo}'", - "adding_new_repo_category": "Avec la catégorie '{category}'.", - "bg_task_custom": "Les dépôts personnalisés sont masqués pendant l'exécution de tâches en arrière-plan.", - "category": "Catégorie", - "compact_mode": "Mode compact", - "custom_repositories": "DÉPÔTS PERSONNALISÉS", - "delete": "Supprimer", - "display": "Affichage", - "grid": "Grille", - "hacs_repo": "Dépôt HACS", - "hidden_repositories": "dépôts cachés", - "missing_category": "Vous devez sélectionner une catégorie", - "open_repository": "Ouvrir dépôt", - "reload_data": "Recharger les données", - "reload_window": "Recharger la fenêtre", - "repository_configuration": "Configuration de dépôt", - "save": "Enregistrer", - "table": "Tableau", - "table_view": "Vue table", - "unhide": "Afficher", - "upgrade_all": "Tout mettre à jour" - }, - "store": { - "ascending": "ascendant", - "clear_new": "Effacer tous les nouveaux dépôts", - "descending": "descendant", - "last_updated": "Dernière mise à jour", - "name": "Nom", - "new_repositories": "Nouveaux dépôts", - "pending_upgrades": "Mises à niveau en attente", - "placeholder_search": "Veuillez entrer un terme de recherche...", - "sort": "Trier", - "stars": "Étoiles", - "status": "Statut" - }, - "time": { - "ago": "il y a", - "day": "jour", - "days": "jours", - "hour": "heure", - "hours": "heures", - "minute": "minute", - "minutes": "minutes", - "month": "mois", - "months": "mois", - "one": "Un", - "one_day_ago": "il y a 1 jour", - "one_hour_ago": "il y a 1 heure", - "one_minute_ago": "il y a 1 minute", - "one_month_ago": "il y a 1 mois", - "one_second_ago": "il y a 1 seconde", - "one_year_ago": "il y a 1 an", - "second": "seconde", - "seconds": "secondes", - "x_days_ago": "il y a {x} jours", - "x_hours_ago": "il y a {x} heures", - "x_minutes_ago": "il y a {x} minutes", - "x_months_ago": "il y a {x} mois", - "x_seconds_ago": "il y a {x} secondes", - "x_years_ago": "il y a {x} ans", - "year": "an", - "years": "ans" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/hu.json b/custom_components/hacs/.translations/hu.json deleted file mode 100644 index 657e261..0000000 --- a/custom_components/hacs/.translations/hu.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "common": { - "about": "Névjegy", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Appok", - "background_task": "Éppen háttérfeladat fut, ez az oldal frissülni fog, ha kész.", - "check_log_file": "További részletekért ellenőrizd a naplófájlt.", - "continue": "Folytatás", - "disabled": "Tiltva", - "documentation": "Dokumentáció", - "hacs_is_disabled": "A HACS le van tiltva", - "installed": "Telepített", - "integration": "Integráció", - "integrations": "Integrációk", - "manage": "kezelés", - "plugin": "Bővítmény", - "plugins": "Bővítmények", - "python_script": "Python Szkript", - "python_scripts": "Python Szkriptek", - "repositories": "Tárolók", - "settings": "beállítások", - "theme": "Téma", - "themes": "Témák", - "version": "Verzió" - }, - "config": { - "abort": { - "single_instance_allowed": "Csak egyetlen HACS-konfiguráció megengedett." - }, - "error": { - "auth": "A Személyes Hozzáférési Token nem megfelelő." - }, - "step": { - "user": { - "data": { - "appdaemon": "AppDaemon appok felfedezésének és nyomon követésének engedélyezése", - "python_script": "Python szkriptek felfedezésének és nyomon követésének engedélyezése", - "sidepanel_icon": "Oldalsó panel ikon", - "sidepanel_title": "Oldalsó panel cím", - "theme": "Témák felfedezésének és nyomon követésének engedélyezése", - "token": "GitHub Személyes Hozzáférési Token" - }, - "description": "Ha segítségre van szükséged a konfigurációval kapcsolatban, akkor tekintsd meg ezt az oldalt: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Biztosan hozzá szeretnéd ezt adni a Lovelace erőforrásokhoz?", - "bg_task": "A művelet le van tiltva, amíg háttérfeladat fut.", - "cancel": "Mégse", - "continue": "Biztosan folytatni szeretnéd?", - "delete": "Biztosan törölni szeretnéd a(z) '{item}'-t?", - "delete_installed": "A(z) '{item}' telepítve van, törlés előtt el kell távolítanod.", - "exist": "{item} már létezik", - "generic": "Biztos vagy benne?", - "home_assistant_is_restarting": "Várj, a Home Assistant éppen újraindul.", - "home_assistant_version_not_correct": "A Home Assistant '{haversion}' verzióját használod, de ehhez a tárolóhoz legalább a(z) '{minversion}' verzióra van szükség.", - "no": "Nem", - "no_upgrades": "Nincs függőben lévő frissítés", - "ok": "OK", - "overwrite": "Ezzel felül fogod írni.", - "reload_data": "Ez újratölti a HACS által ismert összes tároló adatát, ami némi időbe telhet.", - "restart_home_assistant": "Biztosan újraindítod a Home Assistant programot?", - "uninstall": "Biztosan el szeretnéd távolítani a(z) '{item}'-t?", - "upgrade_all": "Ez frissíteni fogja az összes tárolót. Győzödj meg róla, hogy elolvastad az összes kiadási megjegyzést, mielőtt továbblépnél.", - "yes": "Igen" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "AppDaemon appok felfedezésének és nyomon követésének engedélyezése", - "country": "Szűrés országkóddal.", - "debug": "Hibakeresés engedélyezése.", - "experimental": "Kísérleti funkciók engedélyezése", - "not_in_use": "YAML-lel nem használható", - "release_limit": "Megjelenítendő kiadások száma.", - "sidepanel_icon": "Oldalsó panel ikon", - "sidepanel_title": "Oldalsó panel cím" - } - } - } - }, - "repository_banner": { - "config_flow": "Ez az integráció támogatja a config_flow-t, vagyis a felhasználói felületen az integrációk menüben lehet konfigurálni.", - "config_flow_title": "A felhasználói felület konfigurációja támogatott", - "integration_not_loaded": "Ez az integráció nincs betöltve a Home Assistantban.", - "no_restart_required": "Nincs szükség újraindításra", - "not_loaded": "Nincs betöltve", - "plugin_not_loaded": "Ez a bővítmény nincs hozzáadva a Lovelace erőforrásokhoz.", - "restart": "Indítsd újra a Home Assistant programot.", - "restart_pending": "Újraindítás függőben" - }, - "repository": { - "add_to_lovelace": "Hozzáadás a Lovelace-hez", - "authors": "Szerzők", - "available": "Elérhető", - "back_to": "Vissza -", - "changelog": "Változási napló", - "downloads": "Letöltések", - "flag_this": "Megjelölés", - "frontend_version": "Frontend verzió", - "github_stars": "GitHub csillagok", - "goto_integrations": "Ugrás az integrációkhoz", - "hide": "Elrejtés", - "hide_beta": "Béta elrejtése", - "install": "Telepítés", - "installed": "Telepített", - "lovelace_copy_example": "Példa másolása a vágólapra", - "lovelace_instruction": "Amikor hozzáadod ezt a lovelace konfigurációdhoz, használd ezt", - "lovelace_no_js_type": "Nem sikerült meghatározni a beépülő modul típusát, ellenőrizd a tárolót.", - "newest": "legújabb", - "note_appdaemon": "de még hozzá kell adnod az 'apps.yaml' fájlhoz", - "note_installed": "Telepítéskor a következő helyre kerül:", - "note_integration": "de még hozzá kell adnod a 'configuration.yaml' fájlhoz", - "note_plugin": "de még hozzá kell adnod a lovelace konfigurációhoz (az 'ui-lovelace.yaml' fájlban vagy a Lovelace felületen a konfiguráció szerkesztőben)", - "open_issue": "Probléma jelentése", - "open_plugin": "Bővítmény megnyitása", - "reinstall": "Újratelepítés", - "repository": "Tároló", - "restart_home_assistant": "Home Assistant újraindítása", - "show_beta": "Béta megjelenítése", - "uninstall": "Eltávolítás", - "update_information": "Frissítési információk", - "upgrade": "Frissítés" - }, - "settings": { - "add_custom_repository": "EGYÉNI TÁROLÓ HOZZÁADÁSA", - "adding_new_repo": "Új tároló hozzáadása '{repo}'", - "adding_new_repo_category": "A '{category}' kategóriával.", - "bg_task_custom": "Az egyéni tárolók rejtve vannak, amíg háttérfeladat fut.", - "category": "Kategória", - "compact_mode": "Kompakt mód", - "custom_repositories": "EGYÉNI TÁROLÓK", - "delete": "Törlés", - "display": "Megjelenítés", - "grid": "Rács", - "hacs_repo": "HACS tároló", - "hidden_repositories": "rejtett tárolók", - "missing_category": "Ki kell választanod egy kategóriát", - "open_repository": "Tároló megnyitása", - "reload_data": "Adatok újratöltése", - "reload_window": "Ablak újratöltése", - "repository_configuration": "Tároló konfiguráció", - "save": "Mentés", - "table": "Táblázat", - "table_view": "Táblázat nézet", - "unhide": "felfedés", - "upgrade_all": "Minden frissítése" - }, - "store": { - "ascending": "növekvő", - "clear_new": "Új tárolók megjelölése látottként", - "descending": "csökkenő", - "last_updated": "Utolsó frissítés", - "name": "Név", - "new_repositories": "Új tárolók", - "pending_upgrades": "Függőben lévő frissítések", - "placeholder_search": "Kérlek adj meg egy keresési kifejezést...", - "sort": "rendezés", - "stars": "Csillag", - "status": "Állapot" - }, - "time": { - "ago": "ezelőtt", - "day": "nap", - "days": "nap", - "hour": "óra", - "hours": "óra", - "minute": "perc", - "minutes": "perc", - "month": "hónap", - "months": "hónap", - "one": "Egy", - "one_day_ago": "egy nappal ezelőtt", - "one_hour_ago": "egy órával ezelőtt", - "one_minute_ago": "egy perccel ezelőtt", - "one_month_ago": "egy hónappal ezelőtt", - "one_second_ago": "egy másodperccel ezelőtt", - "one_year_ago": "egy évvel ezelőtt", - "second": "másodperc", - "seconds": "másodperc", - "x_days_ago": "{x} nappal ezelőtt", - "x_hours_ago": "{x} órával ezelőtt", - "x_minutes_ago": "{x} perccel ezelőtt", - "x_months_ago": "{x} hónappal ezelőtt", - "x_seconds_ago": "{x} másodperccel ezelőtt", - "x_years_ago": "{x} évvel ezelőtt", - "year": "év", - "years": "év" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/it.json b/custom_components/hacs/.translations/it.json deleted file mode 100644 index fc42529..0000000 --- a/custom_components/hacs/.translations/it.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "common": { - "about": "Informazioni su", - "appdaemon": "AppDaemon", - "appdaemon_apps": "Applicazioni AppDaemon", - "background_task": "Attività in esecuzione, questa pagina sarà ricaricata al termine.", - "check_log_file": "Controlla il tuo file di registro per maggiori dettagli.", - "continue": "Continua", - "disabled": "Disabilitato", - "documentation": "Documentazione", - "hacs_is_disabled": "HACS è disabilitato", - "installed": "Installati", - "integration": "Integrazione", - "integrations": "Integrazioni", - "manage": "gestione", - "netdaemon": "NetDaemon", - "netdaemon_apps": "Applicazioni NetDaemon", - "plugin": "Plugin", - "plugins": "Plugin", - "python_script": "Script python", - "python_scripts": "Script python", - "repositories": "Repository", - "settings": "Impostazioni", - "theme": "Tema", - "themes": "Temi", - "version": "Versione" - }, - "config": { - "abort": { - "single_instance_allowed": "È consentita una sola configurazione di HACS." - }, - "error": { - "auth": "Il token di accesso personale non è corretto." - }, - "step": { - "user": { - "data": { - "appdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni AppDaemon", - "netdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni NetDaemon", - "python_script": "Abilita il rilevamento e il monitoraggio dei python_scripts", - "sidepanel_icon": "Icona nel pannello laterale", - "sidepanel_title": "Titolo nel pannello laterale", - "theme": "Abilita individuazione e tracciamento dei temi", - "token": "Token di accesso personale GitHub" - }, - "description": "Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Sei sicuro di voler aggiungerlo alle tue risorse Lovelace?", - "bg_task": "L'azione è disabilitata mentre sono in esecuzione attività in background.", - "cancel": "Annulla", - "continue": "Sei sicuro di voler continuare?", - "delete": "Sei sicuro di voler disinstallare '{item}'?", - "delete_installed": "'{item}' è installato, è necessario disinstallarlo prima di poterlo eliminare.", - "exist": "{item} esiste già", - "generic": "Sei sicuro?", - "home_assistant_is_restarting": "Aspetta, Home Assistant si sta riavviando.", - "home_assistant_version_not_correct": "Stai eseguendo la versione Home Assistant '{haversion}', ma questo repository richiede l'installazione della versione minima '{minversion}'.", - "no": "No", - "no_upgrades": "Nessun aggiornamento in sospeso", - "ok": "OK", - "overwrite": "In questo modo lo sovrascriverà.", - "reload_data": "Questo ricarica i dati di tutte le repository di cui HACS è a conoscenza, ci vorrà del tempo per finire.", - "restart_home_assistant": "Sei sicuro di voler riavviare Home Assistant?", - "uninstall": "Sei sicuro di voler disinstallare '{item}'?", - "upgrade_all": "Questa azione aggiornerà tutti i repository, assicurati di aver letto le note di rilascio prima di procedere.", - "yes": "Sì" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni AppDaemon", - "country": "Filtra con prefisso internazionale.", - "debug": "Abilita debug.", - "experimental": "Abilita funzionalità sperimentali", - "netdaemon": "Abilita il rilevamento e il monitoraggio delle applicazioni NetDaemon", - "not_in_use": "Non in uso con YAML", - "release_limit": "Numero di versioni da mostrare.", - "sidepanel_icon": "Icona nel pannello laterale", - "sidepanel_title": "Titolo nel pannello laterale" - } - } - } - }, - "repository_banner": { - "config_flow": "Questa integrazione supporta config_flow, questo significa che è ora possibile passare alla sezione \"IntegrazionI\" dell'interfaccia utente per la configurazione.", - "config_flow_title": "Configurazione dell'interfaccia utente supportata", - "integration_not_loaded": "Questa integrazione non è caricata in Home Assistant.", - "no_restart_required": "Non è necessario riavviare", - "not_loaded": "Non caricato", - "plugin_not_loaded": "Questo plugin non è stato aggiunto alle risorse di Lovelace.", - "restart": "È necessario riavviare Home Assistant.", - "restart_pending": "Riavvio in attesa" - }, - "repository": { - "add_to_lovelace": "Aggiungi a Lovelace", - "authors": "Autori", - "available": "Disponibile", - "back_to": "Torna a", - "changelog": "Change log", - "downloads": "Downloads", - "flag_this": "Spunta questo", - "frontend_version": "Frontend versione", - "github_stars": "GitHub stelle", - "goto_integrations": "Vai alle Integrazioni", - "hide": "Nascondi", - "hide_beta": "Nascondi beta", - "install": "Installa", - "installed": "Installato", - "lovelace_copy_example": "Copia l'esempio negli appunti", - "lovelace_instruction": "Quando lo aggiungi nella configurazione di lovelace, usa questo", - "lovelace_no_js_type": "Impossibile determinare il tipo di plugin, verificare il repository.", - "newest": "Più recente", - "note_appdaemon": "dovrai aggiungerlo nel file 'apps.yaml'", - "note_installed": "Una volta installato, si troverà in", - "note_integration": "dovrai aggiungerlo nel file 'configuration.yaml'", - "note_plugin": "devi aggiungere la configurazione nel file 'ui-lovelace.yaml' oppure via Editor RAW della UI.", - "open_issue": "Segnala anomalia", - "open_plugin": "Apri plugin", - "reinstall": "Reinstalla", - "repository": "Archivio Software (Repository)", - "restart_home_assistant": "Riavvia Home Assistant", - "show_beta": "Visualizza beta", - "uninstall": "Rimuovi", - "update_information": "Aggiorna informazioni", - "upgrade": "Aggiorna" - }, - "settings": { - "add_custom_repository": "AGGIUNGI REPOSITORY PERSONALIZZATA", - "adding_new_repo": "Aggiunta di un nuovo repository '{repo}'", - "adding_new_repo_category": "Con la categoria '{category}'.", - "bg_task_custom": "I repository personalizzati sono nascosti mentre sono in esecuzione attività in background.", - "category": "Categoria", - "compact_mode": "Modalità compatta", - "custom_repositories": "REPOSITORY PERSONALIZZATE", - "delete": "Cancella", - "display": "Visualizza", - "grid": "Griglia", - "hacs_repo": "HACS repo", - "hidden_repositories": "repository nascosti", - "missing_category": "Devi selezionare una categoria", - "open_repository": "Apri il repository", - "reload_data": "Ricarica i dati", - "reload_window": "Ricarica la finestra", - "repository_configuration": "Configurazione del repository", - "save": "Salva", - "table": "Tabella", - "table_view": "Vista tabella", - "unhide": "Mostra", - "upgrade_all": "Aggiorna tutto" - }, - "store": { - "ascending": "ascendente", - "clear_new": "Ripulisci i nuovi repository", - "descending": "discendente", - "last_updated": "Ultimo aggiornamento", - "name": "Nome", - "new_repositories": "Nuovi repository", - "pending_upgrades": "Aggiornamenti in sospeso", - "placeholder_search": "Inserire un termine di ricerca", - "sort": "Ordinare", - "stars": "Stelle", - "status": "Stato" - }, - "time": { - "ago": "fa", - "day": "giorno", - "days": "giorni", - "hour": "ora", - "hours": "ore", - "minute": "minuto", - "minutes": "minuti", - "month": "mese", - "months": "mesi", - "one": "Un", - "one_day_ago": "un giorno fa", - "one_hour_ago": "un'ora fa", - "one_minute_ago": "un minuto fa", - "one_month_ago": "un mese fa", - "one_second_ago": "un secondo fa", - "one_year_ago": "un anno fa", - "second": "secondo", - "seconds": "secondi", - "x_days_ago": "{x} giorni fa", - "x_hours_ago": "{x} ore fa", - "x_minutes_ago": "{x} minuti fa", - "x_months_ago": "{x} mesi fa", - "x_seconds_ago": "{x} secondi fa", - "x_years_ago": "{x} anni fa", - "year": "anno", - "years": "anni" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/nb.json b/custom_components/hacs/.translations/nb.json deleted file mode 100644 index 0a7ef22..0000000 --- a/custom_components/hacs/.translations/nb.json +++ /dev/null @@ -1,197 +0,0 @@ -{ - "common": { - "about": "Om", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Apper", - "background_task": "Bakgrunnsoppgaven kjører. Denne siden lastes inn på nytt når den er ferdig.", - "check_log_file": "Sjekk loggfilen din for mer informasjon.", - "continue": "Fortsett", - "disabled": "Deaktivert", - "documentation": "dokumentasjon", - "hacs_is_disabled": "HACS er deaktivert", - "installed": "Installert", - "integration": "Integrasjon", - "integrations": "Integrasjoner", - "manage": "manage", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Apper", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python-skript", - "python_scripts": "Python-skript", - "repositories": "Repositories", - "settings": "Innstillinger", - "theme": "Tema", - "themes": "Temaer", - "version": "Versjon" - }, - "config": { - "abort": { - "single_instance_allowed": "Bare en konfigurasjon av HACS er tillatt." - }, - "error": { - "auth": "Personlig tilgangstoken er ikke korrekt." - }, - "step": { - "user": { - "data": { - "appdaemon": "Aktiver oppdagelse og sporing av AppDaemon-apper", - "netdaemon": "Aktiver oppdagelse og sporing av NetDaemon-apper", - "python_script": "Aktiver oppdagelse og sporing av python_scripts", - "sidepanel_icon": "Sidepanel ikon", - "sidepanel_title": "Sidepanel tittel", - "theme": "Aktiver oppdagelse og sporing av temaer", - "token": "GitHub Personal Access Token" - }, - "description": "Hvis du trenger hjelp med konfigurasjonen, ta en titt her: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Er du sikker på at du vil legge dette til i dine Lovelace resources?", - "bg_task": "Handlingen er deaktivert mens bakgrunnsoppgaver kjører.", - "cancel": "Avbryt", - "continue": "Er du sikker på at du vil fortsette?", - "delete": "Er du sikker på at du vil fjerne '{item}'?", - "delete_installed": "'{item}' er installert, du må avinstallere det før du kan slette det.", - "exist": "{item} eksisterer allerede", - "generic": "Er du sikker?", - "home_assistant_is_restarting": "Vent, Home Assistant starter nå på nytt.", - "home_assistant_version_not_correct": "Du kjører Home Assistant '{haversion}', men denne krever minimum versjon '{minversion}' for å bli installert.", - "no": "Nei", - "no_upgrades": "Ingen oppgraderinger tilgjengelig", - "ok": "OK", - "overwrite": "Å gjøre dette vil overskrive det.", - "reload_data": "Dette laster inn dataene til alle repositories HACS vet om, dette vil ta litt tid å fullføre.", - "restart_home_assistant": "Er du sikker på at du vil starte Home Assistant på nytt?", - "uninstall": "Er du sikker på at du vil avinstallere '{item}'?", - "upgrade_all": "Dette vil oppgradere alle disse repositorene, sørg for at du har lest utgivelses notatene for dem alle før du fortsetter.", - "yes": "Ja" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Aktiver oppdagelse og sporing av AppDaemon-apper", - "country": "Filtrer med landskode.", - "debug": "Aktiver debug", - "experimental": "Aktiver eksperimentelle funksjoner", - "netdaemon": "Aktiver oppdagelse og sporing av NetDaemon-apper", - "not_in_use": "Ikke i bruk med YAML", - "release_limit": "Antall utgivelser som skal vises.", - "sidepanel_icon": "Sidepanel ikon", - "sidepanel_title": "Sidepanel tittel" - } - } - } - }, - "repository_banner": { - "config_flow": "Denne integrasjonen støtter config_flow, det betyr at du nå kan gå til integrasjoner i brukergrensesnittet for å konfigurere den.", - "config_flow_title": "UI konfigurasjon støttet", - "integration_not_loaded": "Integrasjonen er ikke lastet inn i Home Assistant.", - "no_restart_required": "Ingen omstart kreves", - "not_loaded": "Ikke lastet inn", - "plugin_not_loaded": "Denne pluginen er ikke lagt til i lovelace under \"resource\" delen av konfigurasjonen.", - "restart": "Du må restart Home Assistant", - "restart_pending": "Restart er nødvendig" - }, - "repository": { - "add_to_lovelace": "Legg til i Lovelace", - "authors": "Laget av", - "available": "Tilgjengelig", - "back_to": "Tilbake til", - "changelog": "Endringslogg", - "downloads": "Nedlastinger", - "flag_this": "Flag dette", - "frontend_version": "Frontend versjon", - "github_stars": "GitHub-stjerner", - "goto_integrations": "Gå til integrasjoner", - "hide": "Skjul", - "hide_beta": "Skjul beta", - "installed": "Installert", - "lovelace_copy_example": "Kopier eksemplet til utklippstavlen", - "lovelace_instruction": "Når du legger til dette i lovelace-konfigurasjonen din, bruk dette", - "lovelace_no_js_type": "Kunne ikke bestemme typen for denne plugin, sjekk repository.", - "newest": "Nyeste", - "note_appdaemon": "du må fortsatt legge den til i 'apps.yaml' filen", - "note_installed": "Når det er installert, vil dette ligge i", - "note_integration": "du må fortsatt legge den til 'configuration.yaml' filen", - "note_plugin": "du må fortsatt legge den til i lovelace-konfigurasjonen ('ui-lovelace.yaml' eller den rå UI-konfigurasjonsredigereren)", - "open_issue": "Meld et problem", - "open_plugin": "Åpne plugin", - "reinstall": "Installer på nytt", - "repository": "Repository", - "restart_home_assistant": "Start Home Assistant på nytt", - "show_beta": "Vis beta", - "uninstall": "Avinstaller", - "update_information": "Oppdater informasjon", - "upgrade": "Oppdater" - }, - "settings": { - "add_custom_repository": "LEGG TIL REPOSITORY", - "adding_new_repo": "Legger til ny repository '{repo}'", - "adding_new_repo_category": "Med kategori '{category}'.", - "bg_task_custom": "Custom repositories er skjult mens bakgrunnsoppgaver kjører.", - "category": "Kategori", - "compact_mode": "Kompakt modus", - "custom_repositories": "TILPASSEDE REPOSITORIER", - "delete": "Slett", - "display": "Vise", - "grid": "Nett", - "hacs_repo": "HACS repo", - "hidden_repositories": "Gjemte repositories", - "missing_category": "Du må velge en kategori", - "open_repository": "Åpne repository", - "reload_data": "Last inn data på nytt", - "reload_window": "Last inn vinduet på nytt", - "repository_configuration": "Repository konfigurasjon", - "save": "Lagre", - "table": "Tabell", - "table_view": "Tabellvisning", - "unhide": "Vis igjen", - "upgrade_all": "Oppgradere alle" - }, - "store": { - "ascending": "stigende", - "clear_new": "Tøm nye repositories", - "descending": "synkende", - "last_updated": "Sist oppdatert", - "name": "Navn", - "new_repositories": "Nye repositories", - "pending_upgrades": "Venter på oppgradering", - "placeholder_search": "Skriv inn et søkeord ...", - "sort": "sorter", - "stars": "Stjerner", - "status": "Status" - }, - "time": { - "ago": "siden", - "day": "dag", - "days": "dager", - "hour": "time", - "hours": "timer", - "minute": "minutt", - "minutes": "minutter", - "month": "måned", - "months": "måneder", - "one": "En", - "one_day_ago": "for en dag siden", - "one_hour_ago": "en time siden", - "one_minute_ago": "ett minutt siden", - "one_month_ago": "en måned siden", - "one_second_ago": "ett sekund siden", - "one_year_ago": "ett år siden", - "second": "sekund", - "seconds": "sekunder", - "x_days_ago": "{x} dager siden", - "x_hours_ago": "{x} timer siden", - "x_minutes_ago": "{x} minutter siden", - "x_months_ago": "{x} måneder siden", - "x_seconds_ago": "{x} sekunder siden", - "x_years_ago": "{x} år siden", - "year": "år", - "years": "år" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/nl.json b/custom_components/hacs/.translations/nl.json deleted file mode 100644 index 5a8de78..0000000 --- a/custom_components/hacs/.translations/nl.json +++ /dev/null @@ -1,197 +0,0 @@ -{ - "common": { - "about": "Over", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Apps", - "background_task": "Achtergrond taak is draaiende, de pagina herhaalt zichzelf wanneer dit klaar is.", - "check_log_file": "Controleer het logbestand voor meer details.", - "continue": "Doorgaan", - "disabled": "Uitgeschakeld", - "documentation": "Documentatie", - "hacs_is_disabled": "HACS is uitgeschakeld", - "installed": "geinstalleerd", - "integration": "Integratie", - "integrations": "Integraties", - "manage": "beheer", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Apps", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python Script", - "python_scripts": "Python Scripts", - "repositories": "Repositories", - "settings": "instellingen", - "theme": "Thema", - "themes": "Themas", - "version": "Versie" - }, - "config": { - "abort": { - "single_instance_allowed": "Je kunt maar een enkele configuratie van HACS tegelijk hebben." - }, - "error": { - "auth": "Persoonlijke Toegang Token is niet correct." - }, - "step": { - "user": { - "data": { - "appdaemon": "Zet AppDaemon apps ontdekken & traceren aan", - "netdaemon": "Zet NetDaemon apps ontdekken & traceren aan", - "python_script": "Zet python_scripts ontdekken & traceren aan", - "sidepanel_icon": "Zijpaneel icoon", - "sidepanel_title": "Zijpaneel titel", - "theme": "Zet Themes ontdekken & traceren aan", - "token": "GitHub Persoonlijke Toegang Token" - }, - "description": "Als je hulp nodig hebt met de configuratie, kun je hier verder kijken: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Weet u zeker dat u dit wilt toevoegen aan uw Lovelace bronnen?", - "bg_task": "Actie is geblokkeerd terwijl achtergrondtaken actief zijn.", - "cancel": "Annuleer", - "continue": "Weet je zeker dat je wilt doorgaan?", - "delete": "Weet u zeker dat u '{item}' wilt verwijderen?", - "delete_installed": "'{item}' is geïnstalleerd, je dient het eerst te deïnstalleren voordat je het kan verwijderen.", - "exist": "{item} bestaat al.", - "generic": "Weet je het zeker?", - "home_assistant_is_restarting": "Een moment alstublieft, Home Assistant is aan het herstarten.", - "no": "Nee", - "no_upgrades": "Geen upgrades in afwachting.", - "ok": "Oké", - "overwrite": "Door dit te doen, wordt het overschreven.", - "reload_data": "Dit zal alle bekende data herladen van alle repositories van HACS. Dit kan even duren", - "restart_home_assistant": "Weet u zeker dat u Home Assistant opnieuw wilt starten?", - "uninstall": "Weet u zeker dat u '{item}' wilt verwijderen?", - "upgrade_all": "Hiermee worden al deze repositories geüpgraded. Zorg ervoor dat u de release-opmerkingen van allen heeft gelezen voordat u doorgaat.", - "yes": "Ja" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Zet AppDaemon apps ontdekken & traceren aan", - "country": "Filter met land code.", - "debug": "Schakel debug in.", - "experimental": "Zet experimentele functies aan", - "netdaemon": "Zet NetDaemon apps ontdekken & traceren aan", - "not_in_use": "Niet in gebruik met YAML", - "release_limit": "Aantal releases om te laten zien.", - "sidepanel_icon": "Zijpaneel icoon", - "sidepanel_title": "Zijpaneel titel" - } - } - } - }, - "repository_banner": { - "config_flow": "Deze integratie ondersteunt config_flow, wat betekent dat u via uw \"Instellingen\" naar \"Integraties\" kunt gaan om het te configureren.", - "config_flow_title": "UI-configuratie ondersteund", - "integration_not_loaded": "Deze integratie wordt niet geladen in Home Assistant.", - "no_restart_required": "Geen herstart vereist", - "not_loaded": "Niet geladen", - "plugin_not_loaded": "Deze plugin wordt niet toegevoegd aan je Lovelace resources.", - "restart": "U moet Home Assistant opnieuw starten.", - "restart_pending": "Wachten op herstart" - }, - "repository": { - "add_to_lovelace": "Toevoegen aan Lovelace", - "authors": "Auteurs", - "available": "Beschikbaar", - "back_to": "Terug naar", - "changelog": "Changelog", - "downloads": "Downloads", - "flag_this": "Vlag dit", - "frontend_version": "Frontend versie", - "github_stars": "GitHub-sterren", - "goto_integrations": "Ga naar integraties", - "hide": "Verberg", - "hide_beta": "Verberg beta", - "install": "Installeer", - "installed": "Geinstalleerd", - "lovelace_copy_example": "Kopier het voorbeeld naar je klembord", - "lovelace_instruction": "Wanneer je dit gaat toevoegen aan je lovelace configuratie gebruik dit", - "lovelace_no_js_type": "Kon niet achterhalen welk type plugin dit is, controleer de repository van de plugin.", - "newest": "nieuwste", - "note_appdaemon": "je moet het nog steeds toevoegen aan je 'apps.yaml' bestand", - "note_installed": "Wanneer geïnstalleerd, staat het in", - "note_integration": "je moet het nog steeds toevoegen aan je 'configuration.yaml' bestand", - "note_plugin": "je moet het nog steeds toevoegen aan je lovelace configuratie ('ui-lovelace.yaml') of raw UI config editor.", - "open_issue": "Meld probleem", - "open_plugin": "Open plugin", - "reinstall": "Herinstalleer", - "repository": "Repository", - "restart_home_assistant": "Start Home Assistant opnieuw", - "show_beta": "Laat beta zien", - "uninstall": "Verwijder", - "update_information": "Update informatie", - "upgrade": "Update" - }, - "settings": { - "add_custom_repository": "VOEG EIGEN REPOSITORY TOE", - "adding_new_repo": "Nieuwe repository '{repo}' toevoegen", - "adding_new_repo_category": "Met categorie '{category}'.", - "bg_task_custom": "Aangepaste repositories zijn verborgen terwijl de achtergrondtaken actief zijn.", - "category": "Categorie", - "compact_mode": "Compacte modus", - "custom_repositories": "EIGEN REPOSITORIES", - "delete": "Verwijder", - "display": "Weergave", - "grid": "Rooster", - "hacs_repo": "HACS repo", - "hidden_repositories": "verborgen repositories", - "missing_category": "Je moet een categorie selecteren.", - "open_repository": "Open repository", - "reload_data": "Herlaad data", - "reload_window": "Herlaad venster", - "repository_configuration": "Repository configuratie", - "save": "Opslaan", - "table": "Tabel", - "table_view": "Tabelweergave", - "unhide": "zichtbaar maken", - "upgrade_all": "Upgrade alles" - }, - "store": { - "ascending": "oplopend", - "clear_new": "Wissen van alle nieuwe repositories", - "descending": "Aflopend", - "last_updated": "Laatste update", - "name": "Naam", - "new_repositories": "Nieuwe Repositories", - "pending_upgrades": "Upgrades in afwachting", - "placeholder_search": "Typ iets om te zoeken...", - "sort": "sorteer", - "stars": "Sterren", - "status": "Status" - }, - "time": { - "ago": "geleden", - "day": "dag", - "days": "dagen", - "hour": "uur", - "hours": "uren", - "minute": "minuut", - "minutes": "minuten", - "month": "maand", - "months": "maanden", - "one": "Eén", - "one_day_ago": "een dag geleden", - "one_hour_ago": "een uur geleden", - "one_minute_ago": "een minuut geleden", - "one_month_ago": "een maand geleden", - "one_second_ago": "een seconde geleden", - "one_year_ago": "een jaar geleden", - "second": "seconde", - "seconds": "seconden", - "x_days_ago": "{x} dagen geleden", - "x_hours_ago": "{x} uur geleden", - "x_minutes_ago": "{x} minuten geleden", - "x_months_ago": "{x} maanden geleden", - "x_seconds_ago": "{x} seconden geleden", - "x_years_ago": "{x} jaar geleden", - "year": "jaar", - "years": "jaren" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/nn.json b/custom_components/hacs/.translations/nn.json deleted file mode 100644 index 5c1fc0c..0000000 --- a/custom_components/hacs/.translations/nn.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "common": { - "about": "Om", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDeamon-appar", - "background_task": "Bakgrunnsoppgåve køyrer. Denne sida kjem til å laste seg omatt når ho er ferdig.", - "check_log_file": "Sjå i loggfila di for meir detaljar.", - "continue": "Hald fram", - "disabled": "Deaktivert", - "documentation": "Dokumentasjon", - "hacs_is_disabled": "HACS er deaktivert", - "installed": "Installert", - "integration": "Integrasjon", - "integrations": "Integrasjonar", - "manage": "Handtere", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDeamon-appar", - "plugin": "Tillegg", - "plugins": "Tillegg", - "python_script": "Pythonskript", - "python_scripts": "Pythonskript", - "repositories": "Repositories", - "settings": "innstillingar", - "theme": "Tema", - "themes": "Tema", - "version": "Versjon" - }, - "config": { - "abort": { - "single_instance_allowed": "Berre éin enkelt konfigurasjon av HACS er tillete." - }, - "error": { - "auth": "Personleg tilgangsnøkkel er ikkje korrekt." - }, - "step": { - "user": { - "data": { - "appdaemon": "Aktiver AppDeamon-appar-oppdaging og sporing", - "netdaemon": "Aktiver NetDeamon-appar-oppdaging og sporing", - "python_script": "Aktiver pythonscript-oppdaging og sporing", - "sidepanel_icon": "Sidepanelikon", - "sidepanel_title": "Sidepaneltittel", - "theme": "Aktiver tema-oppdaging og sporing", - "token": "Personleg GitHub tilgangsnøkkel" - }, - "description": "Dersom du treng hjelp med konfigurasjonen, ta ein kik her: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Er du sikker på at du vil legge til dette i Lovelace-ressursane dine?", - "bg_task": "Handlinga er deaktivert medan bakgrunnsoppgåveene køyrer.", - "cancel": "Avbryt", - "continue": "Er du sikker på at du vil halde fram?", - "delete": "Er du sikker på at du vil slette '{item}'?", - "delete_installed": "'{item}' er installert. Du må avinstallere det før du kan slette det.", - "exist": "{item} eksisterer allereie", - "generic": "Er du sikker?", - "home_assistant_is_restarting": "Vent... Home Assistant starter på nytt no.", - "home_assistant_version_not_correct": "Du køyrer Home Assistant-versjonen '{haversion}', men dette kodedepoet krev minimum versjon '{minversion}' for å bli installert.", - "no": "Nei", - "no_upgrades": "Ingen ventande oppgradringer", - "ok": "OK", - "overwrite": "Ved å gjere dette kjem du til å overskrive.", - "reload_data": "Dette laster inn dataa til depota HACS veit om, og dette vil ta litt tid å fullføre.", - "restart_home_assistant": "Er du sikker på at du vil starte Home Assistant på nytt?", - "uninstall": "Er du sikker på at du vil avinstallere '{item}'?", - "upgrade_all": "Dette kjem til å oppgradere alle depota. Ver sikker på at du har lest alle versjonsmerknadene før du held fram.", - "yes": "Ja" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Aktiver AppDeamon-appar-oppdaging og sporing", - "country": "Filterer med landskode", - "debug": "Aktiver debug.", - "experimental": "Aktiver ekspreimentelle funksjonar", - "netdaemon": "Aktiver NetDeamon-appar-oppdaging og sporing", - "not_in_use": "Kan ikkje brukast saman med YAML", - "release_limit": "Talet på utgivingar", - "sidepanel_icon": "Sidepanelikon", - "sidepanel_title": "Sidepaneltittel" - } - } - } - }, - "repository_banner": { - "config_flow": "Denne integrasjonen støttar config_flow, som betyr at du no kan gå til integrasjonssida i brukargrensesnittet for å konfigurere den.", - "config_flow_title": "UI-konfigurasjon støtta", - "integration_not_loaded": "Integrasjonen er ikkje lasta inn i Home Assistant.", - "no_restart_required": "Ingen omstart kravd", - "not_loaded": "Ikkje lasta", - "plugin_not_loaded": "Tillegget er ikkje lagt til i Lovelace-ressursane dine.", - "restart": "Du må starte Home Assistant på nytt", - "restart_pending": "Omstart venter" - }, - "repository": { - "add_to_lovelace": "Legg til i Lovelace", - "authors": "Forfatter(e)", - "available": "Tilgjengeleg", - "back_to": "Tilbake til", - "changelog": "Endre logg", - "downloads": "Nedlastinger", - "flag_this": "Marker dette", - "frontend_version": "Frontend-versjon", - "github_stars": "GitHub-stjerner", - "goto_integrations": "Gå til integrasjonar", - "hide": "Gøym", - "hide_beta": "Gøym beta", - "install": "Installer", - "installed": "Installert", - "lovelace_copy_example": "Kopier eksempelet til utklippsbreittet ditt", - "lovelace_instruction": "Når du legg til dette i Lovelace-konfigurasjonen din, bruk dette", - "lovelace_no_js_type": "Kunne ikkje slå fast typen til dette tilegget. Sjå i repositoryet.", - "newest": "nyaste", - "note_appdaemon": "du må framleis legge dette til i \"apps.yaml\"-fila di", - "note_installed": "Når dette er installert, kjem den til å vere plassert i", - "note_integration": "du må framleis legge dette til i \"configuration.yaml\"-fila di", - "note_plugin": "du må framleis dette til i Lovelace-konfigurasjonen (\"ui-lovelace.yaml\" eller i rå-brukargrensesnittredigeraren", - "open_issue": "Opne problem", - "open_plugin": "Opne tillegg", - "reinstall": "Installer på nytt", - "repository": "Repository", - "restart_home_assistant": "Start Home Assistant på nytt", - "show_beta": "Vis beta", - "uninstall": "Avinstaller", - "update_information": "Oppdater informasjonen", - "upgrade": "Oppdater" - }, - "settings": { - "add_custom_repository": "LEGG TIL EIN ANNAN REPOSITORY", - "adding_new_repo": "Legger til ny repository '{repo}'", - "adding_new_repo_category": "Med kategori '{category}'.", - "bg_task_custom": "Custom repositories er skjult medan bakgrunnsoppgaver køyrer.", - "category": "Kategori", - "compact_mode": "Kompaktmodus", - "custom_repositories": "VANLEG REPOSITORY", - "delete": "Slett", - "display": "Vis", - "grid": "rutenett", - "hacs_repo": "HACS repo", - "hidden_repositories": "gøymde repositories", - "missing_category": "Du må velje éin kategori", - "open_repository": "Opne repository", - "reload_data": "Last om dataa", - "reload_window": "Last inn vindauget på nytt", - "repository_configuration": "Repository-konfigurasjon", - "save": "Lagre", - "table": "Tavle", - "table_view": "Tabellvisning", - "unhide": "vis", - "upgrade_all": "Oppdater alle" - }, - "store": { - "ascending": "stigande", - "clear_new": "Fjern alle nye repositories", - "descending": "synkande", - "last_updated": "Sist oppdatert", - "name": "Namn", - "new_repositories": "Ny repository", - "pending_upgrades": "Ventande oppgraderinger", - "placeholder_search": "Ver vennleg og skriv inn ei søkefrase", - "sort": "Sorter", - "stars": "Stjerner", - "status": "Status" - }, - "time": { - "ago": "sidan", - "day": "dag", - "days": "dagar", - "hour": "time", - "hours": "timar", - "minute": "minutt", - "minutes": "minutt", - "month": "månad", - "months": "månadar", - "one": "Éin", - "one_day_ago": "for éin dag sidan", - "one_hour_ago": "éin time sidan", - "one_minute_ago": "eitt minutt sidan", - "one_month_ago": "ein månad sidan", - "one_second_ago": "eitt sekund sidan", - "one_year_ago": "eitt år sidan", - "second": "sekund", - "seconds": "sekund", - "x_days_ago": "{x} dagar siden", - "x_hours_ago": "{x} timer sidan", - "x_minutes_ago": "{x} minutt sidan", - "x_months_ago": "{x} månadar sidan", - "x_seconds_ago": "{x} sekund sidan", - "x_years_ago": "{x} år sidan", - "year": "år", - "years": "år" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/pl.json b/custom_components/hacs/.translations/pl.json deleted file mode 100644 index b76329d..0000000 --- a/custom_components/hacs/.translations/pl.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "common": { - "about": "O", - "appdaemon": "AppDaemon", - "appdaemon_apps": "Aplikacje AppDaemon", - "background_task": "Wykonywanie zadania w tle, ta strona zostanie odświeżona, gdy zadanie zostanie ukończone.", - "check_log_file": "Sprawdź plik dziennika, aby uzyskać więcej informacji.", - "continue": "Kontynuuj", - "disabled": "Wyłączony", - "documentation": "Dokumentacja", - "hacs_is_disabled": "HACS jest wyłączony", - "installed": "zainstalowane", - "integration": "Integracja", - "integrations": "Integracje", - "manage": "zarządzaj", - "netdaemon": "NetDaemon", - "netdaemon_apps": "Aplikacje NetDaemon", - "plugin": "Wtyczka", - "plugins": "Wtyczki", - "python_script": "Skrypt Python", - "python_scripts": "Skrypty Python", - "repositories": "Repozytoria", - "settings": "ustawienia", - "theme": "Motyw", - "themes": "Motywy", - "version": "Wersja" - }, - "config": { - "abort": { - "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja HACS." - }, - "error": { - "auth": "Osobisty token dostępu jest nieprawidłowy." - }, - "step": { - "user": { - "data": { - "appdaemon": "Włącz wykrywanie i śledzenie aplikacji AppDaemon", - "netdaemon": "Włącz wykrywanie i śledzenie aplikacji NetDaemon", - "python_script": "Włącz wykrywanie i śledzenie skryptów Python", - "sidepanel_icon": "Ikona w panelu bocznym", - "sidepanel_title": "Tytuł w panelu bocznym", - "theme": "Włącz wykrywanie i śledzenie motywów", - "token": "Osobisty token dostępu GitHub" - }, - "description": "Jeśli potrzebujesz pomocy w konfiguracji, przejdź na stronę: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Na pewno chcesz dodać to do swoich zasobów Lovelace?", - "bg_task": "Akcja jest wyłączona podczas wykonywania zadań w tle.", - "cancel": "Anuluj", - "continue": "Na pewno chcesz kontynuować?", - "delete": "Na pewno chcesz usunąć '{item}'?", - "delete_installed": "'{item}' jest zainstalowany, musisz go odinstalować zanim będziesz mógł go usunąć.", - "exist": "{item} już istnieje", - "generic": "Jesteś pewny?", - "home_assistant_is_restarting": "Poczekaj, Home Assistant jest teraz ponownie uruchamiany.", - "home_assistant_version_not_correct": "Używasz Home Assistant'a w wersji '{haversion}', a to repozytorium wymaga wersji minimum '{minversion}'.", - "no": "Nie", - "no_upgrades": "Brak oczekujących aktualizacji", - "ok": "Ok", - "overwrite": "Spowoduje to zastąpienie istniejącej kopii.", - "reload_data": "To przeładowuje dane wszystkich repozytoriów, o których wie HACS, może to trochę potrwać.", - "restart_home_assistant": "Na pewno chcesz ponownie uruchomić Home Assistant'a?", - "uninstall": "Na pewno chcesz odinstalować '{item}'?", - "upgrade_all": "To uaktualni wszystkie te repozytoria, upewnij się, że przeczytałeś uwagi do wydania dla wszystkich z nich przed kontynuacją.", - "yes": "Tak" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Włącz wykrywanie i śledzenie aplikacji AppDaemon", - "country": "Filtruj według kodu kraju", - "debug": "Włącz debugowanie.", - "experimental": "Włącz funkcje eksperymentalne", - "netdaemon": "Włącz wykrywanie i śledzenie aplikacji NetDaemon", - "not_in_use": "Nieużywany z YAML", - "release_limit": "Liczba wydań do wyświetlenia", - "sidepanel_icon": "Ikona w panelu bocznym", - "sidepanel_title": "Tytuł w panelu bocznym" - } - } - } - }, - "repository_banner": { - "config_flow": "Ta integracja obsługuje config_flow, co oznacza, że możesz teraz przejść do sekcji integracji w interfejsie użytkownika, aby ją skonfigurować.", - "config_flow_title": "Obsługiwana konfiguracja poprzez interfejs użytkownika", - "integration_not_loaded": "Ta integracja nie jest załadowana do Home Assistant'a.", - "no_restart_required": "Ponowne uruchomienie nie jest wymagane", - "not_loaded": "Nie załadowano", - "plugin_not_loaded": "Ta wtyczka nie jest dodana do zasobów Lovelace.", - "restart": "Musisz ponownie uruchomić Home Assistant'a.", - "restart_pending": "Oczekiwanie na ponowne uruchomienie" - }, - "repository": { - "add_to_lovelace": "Dodaj do Lovelace", - "authors": "Autorzy", - "available": "Dostępna", - "back_to": "Wróć do", - "changelog": "Lista zmian", - "downloads": "Ilość pobrań", - "flag_this": "Oflaguj", - "frontend_version": "Wersja frontendu", - "github_stars": "Gwiazdki GitHub", - "goto_integrations": "Przejdź do integracji", - "hide": "Ukryj", - "hide_beta": "Ukryj wydania beta", - "install": "Zainstaluj", - "installed": "Zainstalowano", - "lovelace_copy_example": "Skopiuj przykład do schowka", - "lovelace_instruction": "Interfejs użytkownika użyje tej wtyczki po dodaniu konfiguracji", - "lovelace_no_js_type": "Nie można określić typu tej wtyczki, sprawdź repozytorium.", - "newest": "najnowsza", - "note_appdaemon": "musisz jeszcze dodać aplikację do pliku 'apps.yaml'", - "note_installed": "Po zainstalowaniu dodatek będzie znajdować się w", - "note_integration": "musisz jeszcze dodać integrację do pliku 'configuration.yaml'", - "note_plugin": "musisz jeszcze dodać wtyczkę do konfiguracji interfejsu użytkownika (plik 'ui-lovelace.yaml' lub edytor interfejsu użytkownika)", - "open_issue": "Powiadom o problemie", - "open_plugin": "Otwórz dodatek", - "reinstall": "Przeinstaluj", - "repository": "Repozytorium", - "restart_home_assistant": "Uruchom ponownie Home Assistant'a", - "show_beta": "Wyświetl wydania beta", - "uninstall": "Odinstaluj", - "update_information": "Informacje o aktualizacji", - "upgrade": "Uaktualnij" - }, - "settings": { - "add_custom_repository": "DODAJ REPOZYTORIUM NIESTANDARDOWE", - "adding_new_repo": "Dodawanie nowego repozytorium '{repo}'", - "adding_new_repo_category": "Z kategorią '{category}'.", - "bg_task_custom": "Niestandardowe repozytoria są ukryte podczas wykonywania zadań w tle.", - "category": "Kategoria", - "compact_mode": "Tryb kompaktowy", - "custom_repositories": "REPOZYTORIA NIESTANDARDOWE", - "delete": "Usuń", - "display": "Sposób wyświetlania", - "grid": "Siatka", - "hacs_repo": "Repozytorium HACS", - "hidden_repositories": "ukryte repozytoria", - "missing_category": "Musisz wybrać kategorię", - "open_repository": "Otwórz repozytorium", - "reload_data": "Wczytaj ponownie dane", - "reload_window": "Załaduj ponownie okno", - "repository_configuration": "Konfiguracja repozytorium", - "save": "Zapisz", - "table": "tabela", - "table_view": "Widok tabeli", - "unhide": "pokaż", - "upgrade_all": "Uaktualnij wszystkie" - }, - "store": { - "ascending": "rosnąco", - "clear_new": "Wyczyść wszystkie nowe repozytoria", - "descending": "malejąco", - "last_updated": "Ostatnia aktualizacja", - "name": "Nazwa", - "new_repositories": "Nowe repozytoria", - "pending_upgrades": "Oczekujące aktualizacje", - "placeholder_search": "Wprowadź wyszukiwane hasło...", - "sort": "sortowanie", - "stars": "Gwiazdki", - "status": "Status" - }, - "time": { - "ago": "temu", - "day": "dzień", - "days": "dni", - "hour": "godzina", - "hours": "godziny", - "minute": "minuta", - "minutes": "minuty", - "month": "miesiąc", - "months": "miesięcy", - "one": "Jeden", - "one_day_ago": "jeden dzień temu", - "one_hour_ago": "jedna godzina temu", - "one_minute_ago": "jedna minuta temu", - "one_month_ago": "jeden miesiąc temu", - "one_second_ago": "jedna sekunda temu", - "one_year_ago": "jeden rok temu", - "second": "sekunda", - "seconds": "sekundy", - "x_days_ago": "{x} dni temu", - "x_hours_ago": "{x} godzin(-y) temu", - "x_minutes_ago": "{x} minut(-y) temu", - "x_months_ago": "{x} miesi(-ące\/-ęcy) temu", - "x_seconds_ago": "{x} sekund(-y) temu", - "x_years_ago": "{x} lat(-a) temu", - "year": "rok", - "years": "lata" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/pt-BR.json b/custom_components/hacs/.translations/pt-BR.json deleted file mode 100644 index e7089af..0000000 --- a/custom_components/hacs/.translations/pt-BR.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "common": { - "about": "Sobre", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Apps", - "background_task": "Tarefa em segundo plano em execução, esta página será recarregada quando terminar.", - "continue": "Continuar", - "disabled": "Desativado", - "documentation": "Documentação", - "hacs_is_disabled": "HACS está desativado", - "installed": "instalado", - "integration": "Integração", - "integrations": "Integrações", - "manage": "gerenciar", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Apps", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python Script", - "python_scripts": "Python Scripts", - "repositories": "Repositórios", - "settings": "configurações", - "theme": "Tema", - "themes": "Temas", - "version": "Versão" - }, - "config": { - "abort": { - "single_instance_allowed": "Apenas uma configuração do HACS é permitida." - }, - "error": { - "auth": "Token de acesso pessoal incorreto." - }, - "step": { - "user": { - "data": { - "appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento", - "netdaemon": "Habilitar NetDaemon apps descoberta & rastreamento", - "python_script": "Habilitar python_scripts descoberta & rastreamento", - "sidepanel_icon": "Icone do painel lateral", - "sidepanel_title": "Titulo do painel lateral", - "theme": "Habilitar temas descoberta & rastreamento", - "token": "GitHub Token de acesso pessoal" - }, - "description": "Se você preciar de ajuda com a configuração olhe aqui: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "cancel": "Cancelar", - "continue": "Tem certeza que quer continuar?", - "delete": "Tem certeza de que deseja excluir '{item}'?", - "exist": "{item} já existe", - "home_assistant_is_restarting": "Espere, o Home Assistant está agora a reiniciar.", - "no": "Não", - "ok": "OK", - "restart_home_assistant": "Tem certeza de que deseja reiniciar o Home Assistant?", - "uninstall": "Tem certeza de que deseja desinstalar '{item}'?", - "yes": "Sim" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Habilitar AppDaemon apps descoberta & rastreamento", - "country": "Filtro pelo código do país.", - "experimental": "Ativar recursos experimentais", - "netdaemon": "Habilitar NetDaemon apps descoberta & rastreamento", - "not_in_use": "Não está em uso com o YAML", - "release_limit": "Número de lançamentos a serem exibidos.", - "sidepanel_icon": "Icone do painel lateral", - "sidepanel_title": "Titulo do painel lateral" - } - } - } - }, - "repository_banner": { - "integration_not_loaded": "Esta integração não é carregada no Home Assistant.", - "no_restart_required": "Não é necessário reiniciar", - "not_loaded": "Não carregado", - "restart": "Você precisa reiniciar o Home Assistant.", - "restart_pending": "Reiniciar pendente" - }, - "repository": { - "add_to_lovelace": "Adicionar a Lovelace", - "authors": "Autores", - "available": "Disponível", - "back_to": "Voltar para", - "changelog": "Changelog", - "downloads": "Downloads", - "flag_this": "Sinalizar isso", - "frontend_version": "Versão Frontend", - "github_stars": "Estrelas de GitHub", - "goto_integrations": "Ir para integrações", - "hide": "Esconder", - "hide_beta": "Esconder beta", - "install": "Instalar", - "installed": "Instalado", - "lovelace_copy_example": "Copie este exemplo para seu clipboard", - "lovelace_instruction": "Quando você adicionar isso à sua configuração do lovelace, use este", - "lovelace_no_js_type": "Não foi possível determinar o tipo deste plug-in, verifique o repositório.", - "newest": "O mais novo", - "note_appdaemon": "Você ainda precisa adicioná-lo ao seu arquivo 'apps.yaml'", - "note_installed": "Quando instalado, ele estará localizado em", - "note_integration": "Você ainda precisa adicioná-lo ao seu arquivo 'configuration.yaml'", - "note_plugin": "você ainda precisará adicioná-lo à sua configuração do lovelace ('ui-lovelace.yaml' ou o editor de configuração da interface do usuário)", - "open_issue": "Open issue", - "open_plugin": "Plugin aberto", - "reinstall": "Reinstalar", - "repository": "Repositório", - "restart_home_assistant": "Reiniciar Home Assistant", - "show_beta": "Mostrar beta", - "uninstall": "Desinstalar", - "update_information": "Atualizar informação", - "upgrade": "Melhorar" - }, - "settings": { - "add_custom_repository": "ADICIONAR REPOSITÓRIO PERSONALIZADO", - "adding_new_repo": "Adicionando novo repositório '{repo}'", - "adding_new_repo_category": "Com a categoria '{category}'.", - "category": "Categoria", - "compact_mode": "Modo compacto", - "custom_repositories": "REPOSITÓRIOS PERSONALIZADOS", - "delete": "Deletar", - "display": "Display", - "grid": "Grade", - "hacs_repo": "HACS repo", - "hidden_repositories": "repositórios ocultos", - "missing_category": "Você precisa selecionar uma categoria", - "open_repository": "Repositório aberto", - "reload_data": "Recarregar dados", - "reload_window": "Recarregar janela", - "repository_configuration": "Configuração do Repositório", - "save": "Salvar", - "table": "Tabela", - "table_view": "Vista de mesa", - "unhide": "reexibir", - "upgrade_all": "Atualizar tudo" - }, - "store": { - "ascending": "ascendente", - "clear_new": "Limpar todos os novos repositórios", - "descending": "descendente", - "last_updated": "Última atualização", - "name": "Nome", - "new_repositories": "Novos Repositórios", - "pending_upgrades": "Atualizações pendentes", - "placeholder_search": "Por favor insira um termo de pesquisa...", - "stars": "Estrelas", - "status": "Status" - }, - "time": { - "ago": "atrás", - "day": "dia", - "days": "dias", - "hour": "hora", - "hours": "horas", - "minute": "minuto", - "minutes": "minutos", - "month": "mês", - "months": "meses", - "one": "Um", - "one_day_ago": "um dia atrás", - "one_hour_ago": "uma hora atrás", - "one_minute_ago": "um minuto atrás", - "one_month_ago": "um mês atrás", - "one_second_ago": "um segundo atrás", - "one_year_ago": "um ano atrás", - "second": "segundo", - "seconds": "segundos", - "x_days_ago": "{x} dias atrás", - "x_hours_ago": "{x} horas atrás", - "x_minutes_ago": "{x} minutos atrás", - "x_months_ago": "{x} meses atrás", - "x_seconds_ago": "{x} segundos atrás", - "x_years_ago": "{x} anos atrás", - "year": "ano", - "years": "anos" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/ro.json b/custom_components/hacs/.translations/ro.json deleted file mode 100644 index d6884aa..0000000 --- a/custom_components/hacs/.translations/ro.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "common": { - "about": "Despre", - "appdaemon": "AppDaemon", - "appdaemon_apps": "Aplicații AppDaemon", - "background_task": "Activitatea de fundal se execută, această pagină se va reîncărca atunci când este gata.", - "continue": "Continua", - "disabled": "Dezactivat", - "documentation": "Documentație", - "hacs_is_disabled": "HACS este dezactivat", - "installed": "instalat", - "integration": "Integrare", - "integrations": "Integrări", - "netdaemon": "NetDaemon", - "netdaemon_apps": "Aplicații NetDaemon", - "plugin": "Plugin", - "plugins": "Plugin-uri", - "python_script": "Script Python", - "python_scripts": "Scripturi Python", - "repositories": "Depozite", - "settings": "setări", - "theme": "Temă", - "themes": "Teme", - "version": "Versiune" - }, - "config": { - "abort": { - "single_instance_allowed": "Doar o singură configurație pentru HACS este permisă." - }, - "error": { - "auth": "Token-ul de acces personal nu este corect." - }, - "step": { - "user": { - "data": { - "appdaemon": "Activați descoperirea și urmărirea aplicațiilor AppDaemon", - "netdaemon": "Activați descoperirea și urmărirea aplicațiilor NetDaemon", - "python_script": "Activați descoperirea și urmărirea python_scripts", - "sidepanel_icon": "Pictogramă Panou lateral", - "sidepanel_title": "Titlu panou lateral", - "theme": "Activați descoperirea și urmărirea temelor", - "token": "Token de acces personal GitHub" - }, - "description": "Dacă aveți nevoie de ajutor pentru configurare, uitați-vă aici: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "bg_task": "Acțiunea este dezactivată în timp ce activitățile de fundal se execută.", - "cancel": "Anulare", - "delete": "Sigur doriți să ștergeți '{item}'?", - "exist": "{item} există deja", - "no": "Nu", - "ok": "OK", - "uninstall": "Sigur doriți să dezinstalați '{item}'?", - "yes": "Da" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Activați descoperirea și urmărirea aplicațiilor AppDaemon", - "country": "Filtrează cu codul țării.", - "experimental": "Activați funcțiile experimentale", - "netdaemon": "Activați descoperirea și urmărirea aplicațiilor NetDaemon", - "not_in_use": "Nu este utilizat cu YAML", - "release_limit": "Număr de versiuni afișate.", - "sidepanel_icon": "Pictogramă Panou lateral", - "sidepanel_title": "Titlu panou lateral" - } - } - } - }, - "repository_banner": { - "integration_not_loaded": "Această integrare nu este încărcată în Home Assistant.", - "no_restart_required": "Nu este necesară repornirea", - "restart": "Trebuie să reporniți Home Assistant.", - "restart_pending": "Reporniți în așteptare" - }, - "repository": { - "add_to_lovelace": "Adăugați la Lovelace", - "authors": "Autori", - "available": "Disponibil", - "back_to": "Înapoi la", - "changelog": "Jurnal modificari", - "downloads": "Descărcări", - "flag_this": "Semnalizează", - "frontend_version": "Versiune frontend", - "github_stars": "Stele GitHub", - "hide": "Ascunde", - "hide_beta": "Ascundere beta", - "install": "Instalează", - "installed": "Instalat", - "lovelace_copy_example": "Copiați exemplul în clipboard", - "lovelace_instruction": "Când adăugați acest lucru la configurația lovelace, folosiți acest", - "lovelace_no_js_type": "Nu s-a putut determina tipul acestui plugin, verificați \"repository\".", - "newest": "cele mai noi", - "note_appdaemon": "mai trebuie să o adăugați in fișierul „apps.yaml”", - "note_installed": "Când este instalat, acesta va fi localizat în", - "note_integration": "mai trebuie să o adăugați in fișierul „configuration.yaml”", - "note_plugin": "mai trebuie să o adăugați la configurația lovelace ('ui-lovelace.yaml' sau in editorul UI)", - "open_issue": "Deschidere problemă", - "open_plugin": "Deschide plugin", - "reinstall": "Reinstalare", - "repository": "Depozit", - "restart_home_assistant": "Reporniți Home Assistant", - "show_beta": "Afișare beta", - "uninstall": "Dezinstalare", - "update_information": "Actualizare informație", - "upgrade": "Actualizare" - }, - "settings": { - "add_custom_repository": "ADĂUGARE DEPOZIT PERSONALIZAT", - "adding_new_repo": "Adăugarea unui depozit nou '{repo}'", - "adding_new_repo_category": "Cu categoria '{categoria}'.", - "category": "Categorie", - "compact_mode": "Modul compact", - "custom_repositories": "DEPOZITE PERSONALIZATE", - "delete": "Șterge", - "display": "Afişeaza", - "grid": "Grilă", - "hacs_repo": "HACS repo", - "hidden_repositories": "depozite ascunse", - "missing_category": "Trebuie să selectați o categorie", - "open_repository": "Deschideți depozitul", - "reload_data": "Reîncărcați datele", - "reload_window": "Reîncărcați fereastra", - "repository_configuration": "Configurația depozitului", - "save": "Salveaza", - "table": "Tabel", - "table_view": "Vizualizare tabel", - "unhide": "Unhide", - "upgrade_all": "Actualizați toate" - }, - "store": { - "ascending": "ascendent", - "descending": "descendent", - "last_updated": "Ultima actualizare", - "name": "Nume", - "new_repositories": "Noi depozite", - "placeholder_search": "Vă rugăm să introduceți un termen de căutare ...", - "sort": "fel", - "stars": "Stele", - "status": "Starea" - }, - "time": { - "ago": "în urmă", - "day": "zi", - "days": "zi", - "hour": "oră", - "hours": "ore", - "minute": "minut", - "minutes": "minute", - "month": "lună", - "months": "luni", - "one": "Unu", - "one_day_ago": "acum o zi", - "one_hour_ago": "acum o oră", - "one_minute_ago": "acum un minut", - "one_month_ago": "acum o luna", - "one_second_ago": "acum o secunda", - "one_year_ago": "acum un an", - "second": "al doilea", - "seconds": "secunde", - "x_days_ago": "{x} zile în urmă", - "x_hours_ago": "{x} ore în urmă", - "x_minutes_ago": "{x} minute în urmă", - "x_months_ago": "{x} luni în urmă", - "x_seconds_ago": "{x} secunde în urmă", - "x_years_ago": "{x} ani în urmă", - "year": "an", - "years": "ani" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/ru.json b/custom_components/hacs/.translations/ru.json deleted file mode 100644 index 7cde0bf..0000000 --- a/custom_components/hacs/.translations/ru.json +++ /dev/null @@ -1,193 +0,0 @@ -{ - "common": { - "about": "О проекте", - "appdaemon": "AppDaemon", - "appdaemon_apps": "Приложения AppDaemon", - "background_task": "Выполняется фоновая задача, страница перезагрузится по готовности.", - "check_log_file": "Проверьте логи для получения более подробной информации.", - "continue": "Продолжить", - "disabled": "Отключено", - "documentation": "Документация", - "hacs_is_disabled": "HACS отключен", - "installed": "установлено", - "integration": "Интеграция", - "integrations": "Интеграции", - "manage": "управлять", - "netdaemon": "NetDaemon", - "netdaemon_apps": "Приложения NetDaemon", - "plugin": "Плагин", - "plugins": "Плагины", - "python_script": "Скрипт Python", - "python_scripts": "Скрипты Python", - "repositories": "Репозитории", - "settings": "настройки", - "theme": "Тема", - "themes": "Темы", - "version": "Версия" - }, - "config": { - "abort": { - "single_instance_allowed": "Разрешена только одна настройка HACS." - }, - "error": { - "auth": "Неверный токен персонального доступа." - }, - "step": { - "user": { - "data": { - "appdaemon": "Включить поиск и отслеживание приложений AppDaemon", - "netdaemon": "Включить поиск и отслеживание приложений NetDaemon", - "python_script": "Включить поиск и отслеживание скриптов Python", - "sidepanel_icon": "Иконка в боковом меню", - "sidepanel_title": "Название в боковом меню", - "theme": "Включить поиск и отслеживание тем", - "token": "Персональный токен доступа GitHub" - }, - "description": "Если вам нужна помощь с настройкой, посмотрите: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Вы уверены, что хотите добавить это в ресурсы Lovelace?", - "bg_task": "Действие отключено во время выполнения фоновых задач.", - "cancel": "Отмена", - "continue": "Вы уверены, что хотите продолжить?", - "delete": "Вы уверены, что хотите удалить '{item}'?", - "delete_installed": "'{item}' установлен, вам нужно нажать 'Удалить', чтобы удалить его.", - "exist": "{item} уже существует.", - "generic": "Вы уверены?", - "no": "Нет", - "no_upgrades": "Нет обновлений", - "ok": "ОК", - "overwrite": "После подтверждения файлы будут перезаписаны.", - "reload_data": "Выполняется перезагрузка данных всех репозиториев в HACS, это займет некоторое время.", - "restart_home_assistant": "Вы уверены, что хотите перезапустить Home Assistant?", - "uninstall": "Вы уверены, что хотите удалить '{item}'?", - "yes": "Да" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Включить поиск и отслеживание приложений AppDaemon", - "country": "Фильтр по стране.", - "debug": "Включить отладку.", - "experimental": "Вкл. экспериментальные функции", - "netdaemon": "Включить поиск и отслеживание приложений NetDaemon", - "not_in_use": "Не используется с YAML", - "release_limit": "Число доступных версий.", - "sidepanel_icon": "Иконка в боковом меню", - "sidepanel_title": "Название в боковом меню" - } - } - } - }, - "repository_banner": { - "integration_not_loaded": "Эта интеграция не загружена в Home Assistant.", - "no_restart_required": "Перезагрузка не требуется", - "not_loaded": "Не загружено", - "plugin_not_loaded": "Этот плагин не добавлен к ресурсам Lovelace.", - "restart": "Вам нужно перезапустить Home Assistant.", - "restart_pending": "Ожидается перезапуск" - }, - "repository": { - "add_to_lovelace": "Добавить в Lovelace", - "authors": "Авторы", - "available": "Доступно", - "back_to": "Назад к", - "changelog": "Изменения", - "downloads": "Загрузки", - "flag_this": "Пожаловаться", - "frontend_version": "Версия", - "github_stars": "Звезды GitHub", - "goto_integrations": "Перейти к интеграции", - "hide": "Скрыть", - "hide_beta": "Скрыть бета", - "install": "Установить", - "installed": "Установлено", - "lovelace_copy_example": "Скопируйте пример в буфер обмена", - "lovelace_instruction": "Для добавления в конфигурацию Lovelace, используйте", - "lovelace_no_js_type": "Не удалось определить тип этого плагина, проверьте репозиторий.", - "newest": "новейшая", - "note_appdaemon": "вам всё ещё нужно добавить код для настройки приложения в файл 'apps.yaml'", - "note_installed": "После установки, файлы будут расположены в", - "note_integration": "вам всё ещё нужно добавить код для настройки интеграции в файл 'configuration.yaml'", - "note_plugin": "вам всё ещё нужно добавить код для настройки плагина в конфигурацию Lovelace ('ui-lovelace.yaml')", - "open_issue": "Сообщить о проблеме", - "open_plugin": "Открыть плагин", - "reinstall": "Переустановить", - "repository": "Репозиторий", - "restart_home_assistant": "Перезагрузка Home Assistant", - "show_beta": "Показать бета", - "uninstall": "Удалить", - "update_information": "Обновить информацию", - "upgrade": "Обновить" - }, - "settings": { - "add_custom_repository": "ДОБАВИТЬ СВОЙ РЕПОЗИТОРИЙ", - "adding_new_repo": "Добавление нового репозитория '{repo}'", - "adding_new_repo_category": "С категорией '{category}'.", - "bg_task_custom": "Свои репозитории скрыты во время выполнения фоновых задач.", - "category": "Категория", - "compact_mode": "Компактный режим", - "custom_repositories": "СВОИ РЕПОЗИТОРИИ", - "delete": "Удалить", - "display": "Вид", - "grid": "Сетка", - "hacs_repo": "Репозиторий HACS", - "hidden_repositories": "Скрытые репозитории", - "missing_category": "Вы должны выбрать категорию", - "open_repository": "Открыть репозиторий", - "reload_data": "Перезагрузить", - "reload_window": "Перезагрузить окно", - "repository_configuration": "Конфигурация репозитория", - "save": "Сохранить", - "table": "Таблица", - "table_view": "Таблица", - "unhide": "Показать", - "upgrade_all": "Обновить всё" - }, - "store": { - "ascending": "по возрастанию", - "clear_new": "Очистить все новые репозитории", - "descending": "по убыванию", - "last_updated": "Последнее обновление", - "name": "Название", - "new_repositories": "Новые репозитории", - "pending_upgrades": "Ожидается обновление", - "placeholder_search": "Пожалуйста, введите условие для поиска...", - "sort": "Сортировка", - "stars": "Звезды", - "status": "Статус" - }, - "time": { - "ago": "назад", - "day": "день", - "days": "дней", - "hour": "час", - "hours": "часов", - "minute": "минута", - "minutes": "минут", - "month": "месяц", - "months": "месяца", - "one": "Одна", - "one_day_ago": "один день назад", - "one_hour_ago": "час назад", - "one_minute_ago": "минуту назад", - "one_month_ago": "месяц назад", - "one_second_ago": "одну секунду назад", - "one_year_ago": "один год назад", - "second": "секунда", - "seconds": "секунд", - "x_days_ago": "{x} дней назад", - "x_hours_ago": "{x} часов назад", - "x_minutes_ago": "{x} минут назад", - "x_months_ago": "{x} месяцев назад", - "x_seconds_ago": "{x} секунд назад", - "x_years_ago": "{x} лет назад", - "year": "год", - "years": "лет" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/sl.json b/custom_components/hacs/.translations/sl.json deleted file mode 100644 index ada106e..0000000 --- a/custom_components/hacs/.translations/sl.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "common": { - "about": "O tem", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon Aplikacije", - "background_task": "V ozadju se izvaja naloga, ko se konča, se bo stran osvežila.", - "check_log_file": "Za več podrobnosti preverite datoteko dnevnika.", - "continue": "Nadaljuj", - "disabled": "Onemogočeno", - "documentation": "Dokumentacija", - "hacs_is_disabled": "HACS je onemogočen", - "installed": "Nameščeno", - "integration": "Integracija", - "integrations": "Integracije", - "manage": "upravljanje", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Aplikacije", - "plugin": "vtičnik", - "plugins": "Vtičniki", - "python_script": "Python skripta", - "python_scripts": "Python skripte", - "repositories": "Repozitoriji", - "settings": "nastavitve", - "theme": "Tema", - "themes": "Teme", - "version": "Različica" - }, - "config": { - "abort": { - "single_instance_allowed": "Dovoljena je samo ena konfiguracija HACS." - }, - "error": { - "auth": "Osebni dostopni žeton ni pravilen." - }, - "step": { - "user": { - "data": { - "appdaemon": "Omogoči odkrivanje in sledenje aplikacij AppDaemon", - "netdaemon": "Omogoči odkrivanje in sledenje aplikacij NetDaemon", - "python_script": "Omogoči odkrivanje in sledenje python_scripts", - "sidepanel_icon": "Ikona stranske plošče", - "sidepanel_title": "Naslov stranske plošče", - "theme": "Omogoči odkrivanje in sledenje tem", - "token": "GitHub žeton za osebni dostop" - }, - "description": "Če potrebujete pomoč pri konfiguraciji, poglejte tukaj: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Ali si prepričan, da želiš to dodati med svoje vire Lovelace?", - "bg_task": "Dejanje je onemogočeno, medtem ko se izvajajo opravila v ozadju.", - "cancel": "Prekliči", - "continue": "Ali ste prepričani, da želite nadaljevati?", - "delete": "Ali ste prepričani, da želite izbrisati '{item}'?", - "delete_installed": "'{item}' je nameščen, pred odstranitvijo ga morate odstraniti.", - "exist": "{item} že obstaja", - "generic": "Ali si prepričan?", - "home_assistant_is_restarting": "Počakaj, Home Assistant se zdaj znova zaganja.", - "home_assistant_version_not_correct": "Uporabljate Home Assistant verzije '{haversion}', vendar to skladišče zahteva nameščeno najmanj različico '{minversion}'.", - "no": "Ne", - "no_upgrades": "Ni nadgradenj", - "ok": "V redu", - "overwrite": "Če to storite, bo to prepisano.", - "reload_data": "Ponovno se naložijo podatki vseh shramb HA, to bo trajalo nekaj časa.", - "restart_home_assistant": "Ali ste prepričani, da želite znova zagnati Home Assistant?", - "uninstall": "Ali ste prepričani, da želite odstraniti '{item}'?", - "upgrade_all": "To bo nadgradil vse te repozitorije, preden nadaljujete poskrbite, da ste prebrali vse opombe ob izdaji.", - "yes": "Da" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Omogoči odkrivanje in sledenje aplikacij AppDaemon", - "country": "Filtrirajte s kodo države.", - "debug": "Omogoči odpravljanje napak.", - "experimental": "Omogočite poskusne funkcije", - "netdaemon": "Omogoči odkrivanje in sledenje aplikacij NetDaemon", - "not_in_use": "Ni v uporabi z YAML", - "release_limit": "Število izdaj, ki jih želite prikazati.", - "sidepanel_icon": "Ikona stranske plošče", - "sidepanel_title": "Naslov stranske plošče" - } - } - } - }, - "repository_banner": { - "config_flow": "Ta integracija podpira config_flow, kar pomeni, da lahko zdaj greste na odsek integracije vašega uporabniškega vmesnika, da ga konfigurirate.", - "config_flow_title": "Konfiguracija uporabniškega vmesnika je podprta", - "integration_not_loaded": "Ta integracija ni naložena v programu Home Assistant.", - "no_restart_required": "Ponovni zagon ni potreben", - "not_loaded": "Ni naloženo", - "plugin_not_loaded": "Ta vtičnik ni dodan vašim virom Lovelace.", - "restart": "Znova morate zagnati Home Assistant.", - "restart_pending": "Vnovični zagon v čakanju" - }, - "repository": { - "add_to_lovelace": "Dodaj v Lovelace", - "authors": "Avtorji", - "available": "Na voljo", - "back_to": "Nazaj na", - "changelog": "Dnevnik sprememb", - "downloads": "Prenosi", - "flag_this": "Označite to", - "frontend_version": "Frontend različica", - "github_stars": "GitHub zvezde", - "goto_integrations": "Pojdite na integracije", - "hide": "Skrij", - "hide_beta": "Skrij različico beta", - "install": "Namestite", - "installed": "Nameščeno", - "lovelace_copy_example": "Kopirajte primer v odložišče", - "lovelace_instruction": "Ko ga boste dodajali v svojo lovelace konfiguracijo uporabite to:", - "lovelace_no_js_type": "Ni bilo mogoče določiti vrsto tega vtičnika, preverite repozitorij.", - "newest": "najnovejše", - "note_appdaemon": "Še vedno ga morate dodati v svojo 'apps.yaml' datoteko", - "note_installed": "Ko bo nameščen, se bo nahajal v", - "note_integration": "Še vedno ga morate dodati v svojo 'configuration.yaml' datoteko", - "note_plugin": "vendar ga še vedno morate dodati v svojo lovelace konfiguracijo ('ui-lovelace.yaml' ali \"raw\" UI config urejevalnik)", - "open_issue": "Odprite težavo", - "open_plugin": "Odprite vtičnik", - "reinstall": "Znova namestite", - "repository": "Repozitorij", - "restart_home_assistant": "Znova zaženite Home Assistant", - "show_beta": "Pokaži različico beta", - "uninstall": "Odstranite", - "update_information": "Posodobite podatke", - "upgrade": "Nadgradnja" - }, - "settings": { - "add_custom_repository": "DODAJTE SVOJ REPOZITORIJ", - "adding_new_repo": "Dodajanje novega repozitorija '{repo}'", - "adding_new_repo_category": "S kategorijo '{category}'.", - "bg_task_custom": "Med izvajanjem nalog v ozadju so repozitoriji po meri skriti.", - "category": "Kategorija", - "compact_mode": "Kompaktni način", - "custom_repositories": "VAŠI REPOZITORIJI", - "delete": "Izbriši", - "display": "Prikaz", - "grid": "Mreža", - "hacs_repo": "HACS repo", - "hidden_repositories": "skriti repozitoriji", - "missing_category": "Izbrati moraš kategorijo", - "open_repository": "Odprite skladišče", - "reload_data": "Ponovno naloži podatke", - "reload_window": "Ponovno naloži okno", - "repository_configuration": "Konfiguracija skladišč", - "save": "Shranite", - "table": "Tabela", - "table_view": "Pogled tabele", - "unhide": "Razkrij", - "upgrade_all": "Nadgradite vse" - }, - "store": { - "ascending": "naraščajoče", - "clear_new": "Počisti vse nove repozitorije", - "descending": "padajoče", - "last_updated": "Nazadnje posodobljeno", - "name": "Ime", - "new_repositories": "Novi repozitoriji", - "pending_upgrades": "Nadgradnje na čakanju", - "placeholder_search": "Prosim vnesite iskalni izraz ...", - "sort": "razvrsti", - "stars": "Zvezd", - "status": "Stanje" - }, - "time": { - "ago": "nazaj", - "day": "dan", - "days": "Dni", - "hour": "ura", - "hours": "Ur", - "minute": "minuta", - "minutes": "minut", - "month": "mesec", - "months": "mesecev", - "one": "Eno", - "one_day_ago": "pred enim dnevom", - "one_hour_ago": "pred eno uro", - "one_minute_ago": "pred eno minuto", - "one_month_ago": "pred enim mesecem", - "one_second_ago": "pred sekundo", - "one_year_ago": "pred enim letom", - "second": "drugič", - "seconds": "sekund", - "x_days_ago": "{x} dni nazaj", - "x_hours_ago": "{x} ur nazaj", - "x_minutes_ago": "{x} minut nazaj", - "x_months_ago": "{x} mesecev nazaj", - "x_seconds_ago": "{x} sekund nazaj", - "x_years_ago": "{x} let nazaj", - "year": "leto", - "years": "Let" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/sv.json b/custom_components/hacs/.translations/sv.json deleted file mode 100644 index e08e9a7..0000000 --- a/custom_components/hacs/.translations/sv.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "common": { - "about": "Om", - "appdaemon": "AppDaemon", - "appdaemon_apps": "Appdaemon Applikationer", - "background_task": "Bakgrundsjobb körs, denna sida kommer att laddas igen när det är klart.", - "continue": "Fortsätta", - "disabled": "Inaktiverad", - "documentation": "Dokumentation", - "hacs_is_disabled": "HACS är inaktiverat", - "installed": "installerad", - "integration": "Integration", - "integrations": "Integrationer", - "netdaemon": "NetDaemon", - "netdaemon_apps": "NetDaemon Applikationer", - "plugin": "Plugin", - "plugins": "Plugins", - "python_script": "Python skript", - "python_scripts": "Python skript", - "repositories": "Repositories", - "settings": "inställningar", - "theme": "Tema", - "themes": "Teman", - "version": "Version" - }, - "config": { - "abort": { - "single_instance_allowed": "Endast en konfiguration kan användas i HACS." - }, - "error": { - "auth": "Personal Access Token är inte korrekt." - }, - "step": { - "user": { - "data": { - "appdaemon": "Upptäck och följ Appdaemon applikationer", - "netdaemon": "Upptäck och följ NetDaemon applikationer", - "python_script": "Upptäck och följ python_scripts", - "sidepanel_icon": "Ikon för sidpanel", - "sidepanel_title": "Rubrik för sidpanel", - "theme": "Upptäck och följ teman", - "token": "GitHub Personal Access Token" - }, - "description": "Om du behöver hjälp med konfigurationen, se här: https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "Är du säker på att du vill lägga till detta till dina Lovelace-resurser?", - "bg_task": "Åtgärden är inaktiverad medan aktiviteter i bakgrunden körs", - "cancel": "Avbryt", - "continue": "Är du säker på att du vill fortsätta?", - "delete": "Är du säker på att du vill ta bort '{item}'?", - "delete_installed": "'{item}' är installerat, du måste avinstallera det innan du kan ta bort det.", - "exist": "{item} existerar redan", - "generic": "Är du säker?", - "no": "Nej", - "no_upgrades": "Inga uppgraderingar väntar", - "ok": "OK", - "overwrite": "Detta kommer att skriva över den.", - "restart_home_assistant": "Är du säker på att du vill starta om Home Assistant?", - "uninstall": "Är du säker på att du vill avinstallera '{item}'?", - "upgrade_all": "Detta kommer uppgradera alla dessa repositories, säkerhetsställ att du läst release anteckningarna för dem alla innan du fortsätter", - "yes": "Ja" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "Upptäck och följ Appdaemon applikationer", - "country": "Filtrera på landskod.", - "debug": "Aktivera felsökning", - "experimental": "Använd experimentella funktioner", - "netdaemon": "Upptäck och följ NetDaemon applikationer", - "release_limit": "Antalet releaser som visas.", - "sidepanel_icon": "Ikon för sidpanel", - "sidepanel_title": "Rubrik för sidpanel" - } - } - } - }, - "repository_banner": { - "integration_not_loaded": "Denna integration inte laddats i Hem Assistent.", - "no_restart_required": "Ingen omstart krävs", - "not_loaded": "Ej laddad", - "plugin_not_loaded": "Detta plugin är inte tillagt till din Lovelaceresurs.", - "restart": "Du måste starta om Home Assistant.", - "restart_pending": "Omstart väntar" - }, - "repository": { - "add_to_lovelace": "Lägg till i Lovelace", - "authors": "Författare", - "available": "Tillgänglig", - "back_to": "Tillbaka till", - "changelog": "Ändringslogg", - "downloads": "Nedladdningar", - "flag_this": "Flagga denna", - "frontend_version": "Frontend-version", - "github_stars": "GitHub stjärnor", - "goto_integrations": "Gå till integrationer", - "hide": "Göm", - "hide_beta": "Göm betaversioner", - "install": "Installera", - "installed": "Installerad", - "lovelace_copy_example": "Kopiera exemplet till urklipp", - "lovelace_instruction": "När du lägger till denna till din lovelace konfiguration, använd", - "lovelace_no_js_type": "Kan inte avgöra villken typ av plugin, kontrollera i GIT \nrepository", - "newest": "nyaste", - "note_appdaemon": "du behöver fortfarande lägga till den till filen 'apps.yaml'", - "note_installed": "När den är installerad kommer den finnas i", - "note_integration": "du behöver fortfarande lägga den till filen 'configuration.yaml'", - "note_plugin": "du behöver fortfarande lägga till den till din lovelace konfiguration ('ui-lovelace.yaml' eller raw UI config redigerare)", - "open_issue": "Rapportera problem", - "open_plugin": "Öppna plugin", - "reinstall": "Ominstallera", - "repository": "Repository", - "restart_home_assistant": "Starta om Home Assistant", - "show_beta": "Visa betaversioner", - "uninstall": "Avinstallera", - "update_information": "Uppdatera information", - "upgrade": "Uppdatera" - }, - "settings": { - "add_custom_repository": "LÄGG TILL ETT REPOSITORY", - "adding_new_repo": "Lägger till nytt repository '{repo}'", - "adding_new_repo_category": "Med kategori '{kategori}'.", - "bg_task_custom": "Anpassade repositories är dolda under tiden aktiviteter i bakgrunden körs", - "category": "Kategori", - "compact_mode": "Kompakt läge", - "custom_repositories": "REPOSITORIES", - "delete": "Ta bort", - "display": "Visa", - "grid": "Rutnät", - "hacs_repo": "HACS repo", - "hidden_repositories": "dolda förråd", - "missing_category": "Du behöver välja en kategori", - "open_repository": "Öppna Repository", - "reload_data": "Ladda om data", - "reload_window": "Ladda om fönstret", - "save": "Spara", - "table": "Tabell", - "table_view": "Tabellvy", - "unhide": "ta fram", - "upgrade_all": "Uppgradera alla" - }, - "store": { - "ascending": "stigande", - "clear_new": "Rensa alla nya förvar", - "last_updated": "Senast uppdaterad", - "name": "Namn", - "new_repositories": "Nya förvar", - "pending_upgrades": "Väntande uppgraderingar", - "placeholder_search": "Skriv ett sökord...", - "sort": "sortera", - "stars": "Stjärnor", - "status": "Status" - }, - "time": { - "ago": "sedan", - "day": "dag", - "days": "dagar", - "hour": "timme", - "hours": "timmar", - "minute": "minut", - "minutes": "minuter", - "month": "månad", - "months": "månader", - "one": "Ett", - "one_day_ago": "för en dag sedan", - "one_hour_ago": "för en timme sedan", - "one_minute_ago": "en minut sedan", - "one_month_ago": "en månad sedan", - "one_second_ago": "för en sekund sedan", - "one_year_ago": "ett år sedan", - "second": "andra", - "seconds": "sekunder", - "x_days_ago": "{x} dagar sedan", - "x_hours_ago": "{x} timmar sedan", - "x_minutes_ago": "{x} minuter sedan", - "x_months_ago": "{x} månader sedan", - "x_seconds_ago": "{x} sekunder sedan", - "x_years_ago": "{x} år sedan", - "year": "år", - "years": "år" - } -} \ No newline at end of file diff --git a/custom_components/hacs/.translations/zh-Hans.json b/custom_components/hacs/.translations/zh-Hans.json deleted file mode 100644 index 73ecd24..0000000 --- a/custom_components/hacs/.translations/zh-Hans.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "common": { - "about": "关于", - "appdaemon": "AppDaemon", - "appdaemon_apps": "AppDaemon应用", - "background_task": "后台任务正在运行,完成后将重新加载此页面。", - "check_log_file": "请查看日志文件以了解更多信息。", - "continue": "继续", - "disabled": "禁用", - "documentation": "文档", - "hacs_is_disabled": "HACS已禁用", - "installed": "已安装", - "integration": "自定义组件", - "integrations": "自定义组件", - "manage": "管理", - "netdaemon": "NetDaemon应用", - "netdaemon_apps": "NetDaemon应用", - "plugin": "Lovelace插件", - "plugins": "Lovelace插件", - "python_script": "Python脚本", - "python_scripts": "Python脚本", - "repositories": "仓库数量", - "settings": "设置", - "theme": "主题", - "themes": "主题", - "version": "版本" - }, - "config": { - "abort": { - "single_instance_allowed": "仅允许单个HACS配置。" - }, - "error": { - "auth": "个人访问令牌不正确。" - }, - "step": { - "user": { - "data": { - "appdaemon": "启用AppDaemon应用发现和跟踪", - "netdaemon": "启用NetDaemon应用发现和跟踪", - "python_script": "启用python_scripts发现和跟踪", - "sidepanel_icon": "侧面板图标", - "sidepanel_title": "侧面板标题", - "theme": "启用主题发现和跟踪", - "token": "GitHub个人访问令牌" - }, - "description": "如果您需要有关配置的帮助,请在此处查看:https:\/\/hacs.xyz\/docs\/configuration\/start", - "title": "HACS (Home Assistant Community Store)" - } - }, - "title": "HACS (Home Assistant Community Store)" - }, - "confirm": { - "add_to_lovelace": "您确定要将此添加到Lovelace资源中吗?", - "bg_task": "后台任务正在运行时,操作被禁用。", - "cancel": "取消", - "continue": "你确定你要继续吗?", - "delete": "是否确实要删除\"{item}\"?", - "delete_installed": "已安装 '{item}',需要先将其卸载,然后才能将其删除。", - "exist": "{item}已经存在", - "generic": "你确定吗?", - "home_assistant_is_restarting": "请等待,Home Assistant现在正在重新启动。", - "home_assistant_version_not_correct": "您正在运行Home Assistant的版本为'{haversion}',但是这个需要安装最低版本是'{minversion}'。", - "no": "取消", - "no_upgrades": "暂无升级", - "ok": "确定", - "overwrite": "这样做会覆盖它。", - "reload_data": "这将重新加载HACS知道的所有仓库的数据,这需要一些时间才能完成。", - "restart_home_assistant": "您确定要重新启动Home Assistant吗?", - "uninstall": "您确定要卸载“ {item} ”吗?", - "upgrade_all": "这将升级所有这些仓库,请确保在继续之前已阅读所有仓库的发行说明。", - "yes": "确定" - }, - "options": { - "step": { - "user": { - "data": { - "appdaemon": "启用AppDaemon应用发现和跟踪", - "country": "用国家代码过滤。", - "debug": "启用调试。", - "experimental": "启用实验功能", - "netdaemon": "启用NetDaemon应用发现和跟踪", - "not_in_use": "不适用于 YAML", - "release_limit": "要显示的发行数量。", - "sidepanel_icon": "侧面板图标", - "sidepanel_title": "侧面板标题" - } - } - } - }, - "repository_banner": { - "config_flow": "此组件支持config_flow,这意味着您现在可以转到UI的“集成”部分进行配置。", - "config_flow_title": "支持UI配置", - "integration_not_loaded": "此集成未加载到Home Assistant中。", - "no_restart_required": "无需重启", - "not_loaded": "未加载", - "plugin_not_loaded": "该插件未添加到您的Lovelace资源中。", - "restart": "您需要重新启动Home Assistant。", - "restart_pending": "重新启动待处理" - }, - "repository": { - "add_to_lovelace": "添加到Lovelace", - "authors": "作者", - "available": "版本号", - "back_to": "返回", - "changelog": "更新日志", - "downloads": "下载", - "flag_this": "标记", - "frontend_version": "前端版本", - "github_stars": "GitHub星级", - "goto_integrations": "转到组件", - "hide": "隐藏", - "hide_beta": "隐藏测试版", - "install": "安装", - "installed": "已安装版本", - "lovelace_copy_example": "复制样例代码到你的剪贴板", - "lovelace_instruction": "您仍然需要将下列代码添加到lovelace配置中(“ ui-lovelace.yaml”或原始配置编辑器):", - "lovelace_no_js_type": "无法确定此插件的类型,请检查仓库。", - "newest": "最新", - "note_appdaemon": "您仍然需要将其添加到“ apps.yaml”文件中", - "note_installed": "安装后,它将位于", - "note_integration": "您仍然需要将其添加到“ configuration.yaml”文件中", - "note_plugin": "您仍然需要将其添加到lovelace配置中(“ ui-lovelace.yaml”或原始配置编辑器)ui-lovelace.yaml", - "open_issue": "提交问题", - "open_plugin": "打开插件", - "reinstall": "重新安装", - "repository": "仓库", - "restart_home_assistant": "重新启动Home Assistant", - "show_beta": "显示测试版", - "uninstall": "卸载", - "update_information": "更新信息", - "upgrade": "升级" - }, - "settings": { - "add_custom_repository": "添加自定义仓库", - "adding_new_repo": "添加新的仓库 '{repo}'", - "adding_new_repo_category": "类别为 '{category}'。", - "bg_task_custom": "自定义仓库在后台任务运行时被隐藏。", - "category": "类别", - "compact_mode": "紧凑模式", - "custom_repositories": "自定义仓库", - "delete": "删除", - "display": "视图方式", - "grid": "网格", - "hacs_repo": "HACS仓库", - "hidden_repositories": "隐藏的仓库", - "missing_category": "您需要选择一个类别", - "open_repository": "打开仓库", - "reload_data": "重载数据", - "reload_window": "重新加载窗口", - "repository_configuration": "仓库数据配置", - "save": "保存", - "table": "列表", - "table_view": "表视图", - "unhide": "取消隐藏", - "upgrade_all": "升级全部" - }, - "store": { - "ascending": "升序", - "clear_new": "清除所有新仓库标记", - "descending": "降序", - "last_updated": "最近更新时间", - "name": "名称", - "new_repositories": "新仓库", - "pending_upgrades": "待升级", - "placeholder_search": "搜索项目...", - "sort": "排序", - "stars": "星级", - "status": "状态" - }, - "time": { - "ago": "过去", - "day": "天", - "days": "天", - "hour": "小时", - "hours": "小时", - "minute": "分", - "minutes": "分", - "month": "月", - "months": "月", - "one": "一", - "one_day_ago": "一天前", - "one_hour_ago": "一个小时之前", - "one_minute_ago": "一分钟前", - "one_month_ago": "一个月前", - "one_second_ago": "一秒钟前", - "one_year_ago": "一年前", - "second": "秒", - "seconds": "秒", - "x_days_ago": "{x}天前", - "x_hours_ago": "{x}小时前", - "x_minutes_ago": "{x}分钟前", - "x_months_ago": "{x}个月前", - "x_seconds_ago": "{x}秒前", - "x_years_ago": "{x}年前", - "year": "年", - "years": "年" - } -} \ No newline at end of file diff --git a/custom_components/hacs/__init__.py b/custom_components/hacs/__init__.py index 1b94fbc..5adcd48 100644 --- a/custom_components/hacs/__init__.py +++ b/custom_components/hacs/__init__.py @@ -1,224 +1,32 @@ -""" -Custom element manager for community created elements. - -For more details about this integration, please refer to the documentation at -https://hacs.xyz/ -""" - -import voluptuous as vol -from aiogithubapi import AIOGitHub -from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.const import __version__ as HAVERSION -from homeassistant.components.lovelace import system_health_info -from homeassistant.exceptions import ConfigEntryNotReady, ServiceNotFound -from homeassistant.helpers.aiohttp_client import async_create_clientsession -from homeassistant.helpers.event import async_call_later - -from custom_components.hacs.configuration_schema import ( - hacs_base_config_schema, - hacs_config_option_schema, -) -from custom_components.hacs.const import DOMAIN, ELEMENT_TYPES, STARTUP, VERSION -from custom_components.hacs.constrains import check_constans, check_requirements -from custom_components.hacs.hacsbase.configuration import Configuration -from custom_components.hacs.hacsbase.data import HacsData -from custom_components.hacs.setup import ( - add_sensor, - load_hacs_repository, - setup_frontend, -) - -from custom_components.hacs.globals import get_hacs - -from custom_components.hacs.helpers.network import internet_connectivity_check - -SCHEMA = hacs_base_config_schema() -SCHEMA[vol.Optional("options")] = hacs_config_option_schema() -CONFIG_SCHEMA = vol.Schema({DOMAIN: SCHEMA}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Set up this integration using yaml.""" - hacs = get_hacs() - if DOMAIN not in config: - return True - hass.data[DOMAIN] = config - hacs.hass = hass - hacs.session = async_create_clientsession(hass) - hacs.configuration = Configuration.from_dict( - config[DOMAIN], config[DOMAIN].get("options") - ) - hacs.configuration.config = config - hacs.configuration.config_type = "yaml" - await startup_wrapper_for_yaml() - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) - ) - return True - - -async def async_setup_entry(hass, config_entry): - """Set up this integration using UI.""" - hacs = get_hacs() - conf = hass.data.get(DOMAIN) - if config_entry.source == config_entries.SOURCE_IMPORT: - if conf is None: - hass.async_create_task( - hass.config_entries.async_remove(config_entry.entry_id) - ) - return False - hacs.hass = hass - hacs.session = async_create_clientsession(hass) - hacs.configuration = Configuration.from_dict( - config_entry.data, config_entry.options - ) - hacs.configuration.config_type = "flow" - hacs.configuration.config_entry = config_entry - config_entry.add_update_listener(reload_hacs) - startup_result = await hacs_startup() - if not startup_result: - hacs.system.disabled = True - raise ConfigEntryNotReady - hacs.system.disabled = False - return startup_result - - -async def startup_wrapper_for_yaml(): - """Startup wrapper for yaml config.""" - hacs = get_hacs() - startup_result = await hacs_startup() - if not startup_result: - hacs.system.disabled = True - hacs.hass.components.frontend.async_remove_panel( - hacs.configuration.sidepanel_title.lower() - .replace(" ", "_") - .replace("-", "_") - ) - hacs.logger.info("Could not setup HACS, trying again in 15 min") - async_call_later(hacs.hass, 900, startup_wrapper_for_yaml()) - return - hacs.system.disabled = False - - -async def hacs_startup(): - """HACS startup tasks.""" - hacs = get_hacs() - if not check_requirements(): - return False - if hacs.configuration.debug: - try: - await hacs.hass.services.async_call( - "logger", "set_level", {"hacs": "debug"} - ) - except ServiceNotFound: - hacs.logger.error( - "Could not set logging level to debug, logger is not enabled" - ) - - lovelace_info = await system_health_info(hacs.hass) - hacs.logger.debug(f"Configuration type: {hacs.configuration.config_type}") - hacs.version = VERSION - hacs.logger.info(STARTUP) - hacs.system.config_path = hacs.hass.config.path() - hacs.system.ha_version = HAVERSION - - hacs.system.lovelace_mode = lovelace_info.get("mode", "yaml") - hacs.system.disabled = False - hacs.github = AIOGitHub( - hacs.configuration.token, async_create_clientsession(hacs.hass) - ) - hacs.data = HacsData() - - # Check HACS Constrains - if not await hacs.hass.async_add_executor_job(check_constans): - if hacs.configuration.config_type == "flow": - if hacs.configuration.config_entry is not None: - await async_remove_entry(hacs.hass, hacs.configuration.config_entry) - return False - - # Set up frontend - await setup_frontend() - - if not await hacs.hass.async_add_executor_job(internet_connectivity_check): - hacs.logger.critical("No network connectivity") - return False - - # Load HACS - if not await load_hacs_repository(): - if hacs.configuration.config_type == "flow": - if hacs.configuration.config_entry is not None: - await async_remove_entry(hacs.hass, hacs.configuration.config_entry) - return False - - # 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) - return False - - # Add aditional categories - hacs.common.categories = ELEMENT_TYPES - if hacs.configuration.appdaemon: - hacs.common.categories.append("appdaemon") - if hacs.configuration.netdaemon: - hacs.common.categories.append("netdaemon") - if hacs.configuration.python_script: - hacs.configuration.python_script = False - if hacs.configuration.config_type == "yaml": - hacs.logger.warning( - "Configuration option 'python_script' is deprecated and you should remove it from your configuration, HACS will know if you use 'python_script' in your Home Assistant configuration, this option will be removed in a future release." - ) - if hacs.configuration.theme: - hacs.configuration.theme = False - if hacs.configuration.config_type == "yaml": - hacs.logger.warning( - "Configuration option 'theme' is deprecated and you should remove it from your configuration, HACS will know if you use 'theme' in your Home Assistant configuration, this option will be removed in a future release." - ) - - # Setup startup tasks - if hacs.configuration.config_type == "yaml": - hacs.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, hacs.startup_tasks()) - else: - async_call_later(hacs.hass, 5, hacs.startup_tasks()) - - # Show the configuration - hacs.configuration.print() - - # Set up sensor - await hacs.hass.async_add_executor_job(add_sensor) - - # Mischief managed! - return True - - -async def async_remove_entry(hass, config_entry): - """Handle removal of an entry.""" - hacs = get_hacs() - hacs.logger.info("Disabling HACS") - hacs.logger.info("Removing recuring tasks") - for task in hacs.recuring_tasks: - task() - hacs.logger.info("Removing sensor") - try: - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - except ValueError: - pass - hacs.logger.info("Removing sidepanel") - try: - hass.components.frontend.async_remove_panel("hacs") - except AttributeError: - pass - hacs.system.disabled = True - hacs.logger.info("HACS is now disabled") - - -async def reload_hacs(hass, config_entry): - """Reload HACS.""" - await async_remove_entry(hass, config_entry) - await async_setup_entry(hass, config_entry) +""" +HACS gives you a powerful UI to handle downloads of all your custom needs. + +For more details about this integration, please refer to the documentation at +https://hacs.xyz/ +""" +import voluptuous as vol + +from .const import DOMAIN +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 + +CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up this integration using yaml.""" + + return await hacs_yaml_setup(hass, config) + + +async def async_setup_entry(hass, config_entry): + """Set up this integration using UI.""" + + return await hacs_ui_setup(hass, config_entry) + + +async def async_remove_entry(hass, config_entry): + """Handle removal of an entry.""" + return await hacs_remove_entry(hass, config_entry) diff --git a/custom_components/hacs/__pycache__/__init__.cpython-37.pyc b/custom_components/hacs/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index f1d39da..0000000 Binary files a/custom_components/hacs/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/config_flow.cpython-37.pyc b/custom_components/hacs/__pycache__/config_flow.cpython-37.pyc deleted file mode 100644 index a2b1bac..0000000 Binary files a/custom_components/hacs/__pycache__/config_flow.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/configuration_schema.cpython-37.pyc b/custom_components/hacs/__pycache__/configuration_schema.cpython-37.pyc deleted file mode 100644 index bad0489..0000000 Binary files a/custom_components/hacs/__pycache__/configuration_schema.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/const.cpython-37.pyc b/custom_components/hacs/__pycache__/const.cpython-37.pyc deleted file mode 100644 index 2165744..0000000 Binary files a/custom_components/hacs/__pycache__/const.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/constrains.cpython-37.pyc b/custom_components/hacs/__pycache__/constrains.cpython-37.pyc deleted file mode 100644 index 133fc09..0000000 Binary files a/custom_components/hacs/__pycache__/constrains.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/globals.cpython-37.pyc b/custom_components/hacs/__pycache__/globals.cpython-37.pyc deleted file mode 100644 index 9e25b39..0000000 Binary files a/custom_components/hacs/__pycache__/globals.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/http.cpython-37.pyc b/custom_components/hacs/__pycache__/http.cpython-37.pyc deleted file mode 100644 index f0815ad..0000000 Binary files a/custom_components/hacs/__pycache__/http.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/sensor.cpython-37.pyc b/custom_components/hacs/__pycache__/sensor.cpython-37.pyc deleted file mode 100644 index 9b992fd..0000000 Binary files a/custom_components/hacs/__pycache__/sensor.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/setup.cpython-37.pyc b/custom_components/hacs/__pycache__/setup.cpython-37.pyc deleted file mode 100644 index d7d1a89..0000000 Binary files a/custom_components/hacs/__pycache__/setup.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/store.cpython-37.pyc b/custom_components/hacs/__pycache__/store.cpython-37.pyc deleted file mode 100644 index 7f3b605..0000000 Binary files a/custom_components/hacs/__pycache__/store.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/__pycache__/ws_api_handlers.cpython-37.pyc b/custom_components/hacs/__pycache__/ws_api_handlers.cpython-37.pyc deleted file mode 100644 index 56c1ad4..0000000 Binary files a/custom_components/hacs/__pycache__/ws_api_handlers.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/api/__init__.py b/custom_components/hacs/api/__init__.py new file mode 100644 index 0000000..f8017b3 --- /dev/null +++ b/custom_components/hacs/api/__init__.py @@ -0,0 +1 @@ +"""Initialize HACS API""" diff --git a/custom_components/hacs/api/acknowledge_critical_repository.py b/custom_components/hacs/api/acknowledge_critical_repository.py new file mode 100644 index 0000000..02235b2 --- /dev/null +++ b/custom_components/hacs/api/acknowledge_critical_repository.py @@ -0,0 +1,25 @@ +"""API Handler for acknowledge_critical_repository""" +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, + async_save_to_store, +) + + +@websocket_api.async_response +@websocket_api.websocket_command( + {vol.Required("type"): "hacs/critical", vol.Optional("repository"): cv.string} +) +async def acknowledge_critical_repository(hass, connection, msg): + """Handle get media player cover command.""" + repository = msg["repository"] + + critical = await async_load_from_store(hass, "critical") + for repo in critical: + if repository == repo["repository"]: + repo["acknowledged"] = True + await async_save_to_store(hass, "critical", critical) + connection.send_message(websocket_api.result_message(msg["id"], critical)) diff --git a/custom_components/hacs/api/check_local_path.py b/custom_components/hacs/api/check_local_path.py new file mode 100644 index 0000000..308b81b --- /dev/null +++ b/custom_components/hacs/api/check_local_path.py @@ -0,0 +1,24 @@ +"""API Handler for check_local_path""" +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 + + +@websocket_api.async_response +@websocket_api.websocket_command( + {vol.Required("type"): "hacs/check_path", vol.Optional("path"): cv.string} +) +async def check_local_path(_hass, connection, msg): + """Handle get media player cover command.""" + path = msg.get("path") + exist = {"exist": False} + + if path is None: + return + + if await async_path_exsist(path): + exist["exist"] = True + + connection.send_message(websocket_api.result_message(msg["id"], exist)) diff --git a/custom_components/hacs/api/get_critical_repositories.py b/custom_components/hacs/api/get_critical_repositories.py new file mode 100644 index 0000000..35f2c94 --- /dev/null +++ b/custom_components/hacs/api/get_critical_repositories.py @@ -0,0 +1,15 @@ +"""API Handler for get_critical_repositories""" +import voluptuous as vol +from homeassistant.components import websocket_api + +from custom_components.hacs.helpers.functions.store import async_load_from_store + + +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "hacs/get_critical"}) +async def get_critical_repositories(hass, connection, msg): + """Handle get media player cover command.""" + critical = await async_load_from_store(hass, "critical") + if not critical: + critical = [] + connection.send_message(websocket_api.result_message(msg["id"], critical)) diff --git a/custom_components/hacs/api/hacs_config.py b/custom_components/hacs/api/hacs_config.py new file mode 100644 index 0000000..1760ee8 --- /dev/null +++ b/custom_components/hacs/api/hacs_config.py @@ -0,0 +1,28 @@ +"""API Handler for hacs_config""" +import voluptuous as vol +from homeassistant.components import websocket_api + +from custom_components.hacs.share import get_hacs + + +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "hacs/config"}) +async def hacs_config(_hass, connection, msg): + """Handle get media player cover command.""" + hacs = get_hacs() + config = hacs.configuration + + content = {} + content["frontend_mode"] = config.frontend_mode + content["frontend_compact"] = config.frontend_compact + content["onboarding_done"] = config.onboarding_done + content["version"] = hacs.version + content["frontend_expected"] = hacs.frontend.version_expected + content["frontend_running"] = hacs.frontend.version_running + content["dev"] = config.dev + content["debug"] = config.debug + content["country"] = config.country + content["experimental"] = config.experimental + content["categories"] = hacs.common.categories + + connection.send_message(websocket_api.result_message(msg["id"], content)) diff --git a/custom_components/hacs/api/hacs_removed.py b/custom_components/hacs/api/hacs_removed.py new file mode 100644 index 0000000..28628e8 --- /dev/null +++ b/custom_components/hacs/api/hacs_removed.py @@ -0,0 +1,15 @@ +"""API Handler for hacs_removed""" +import voluptuous as vol +from homeassistant.components import websocket_api + +from custom_components.hacs.share import list_removed_repositories + + +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "hacs/removed"}) +async def hacs_removed(_hass, connection, msg): + """Get information about removed repositories.""" + content = [] + for repo in list_removed_repositories(): + content.append(repo.to_json()) + connection.send_message(websocket_api.result_message(msg["id"], content)) diff --git a/custom_components/hacs/api/hacs_repositories.py b/custom_components/hacs/api/hacs_repositories.py new file mode 100644 index 0000000..e003342 --- /dev/null +++ b/custom_components/hacs/api/hacs_repositories.py @@ -0,0 +1,62 @@ +"""API Handler for hacs_repositories""" +import voluptuous as vol +from homeassistant.components import websocket_api + +from custom_components.hacs.share import get_hacs + + +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "hacs/repositories"}) +async def hacs_repositories(_hass, connection, msg): + """Handle get media player cover command.""" + hacs = get_hacs() + repositories = hacs.repositories + content = [] + for repo in repositories: + if repo.data.category in hacs.common.categories: + data = { + "additional_info": repo.information.additional_info, + "authors": repo.data.authors, + "available_version": repo.display_available_version, + "beta": repo.data.show_beta, + "can_install": repo.can_install, + "category": repo.data.category, + "country": repo.data.country, + "config_flow": repo.data.config_flow, + "custom": repo.custom, + "default_branch": repo.data.default_branch, + "description": repo.data.description, + "domain": repo.data.domain, + "downloads": repo.data.downloads, + "file_name": repo.data.file_name, + "first_install": repo.status.first_install, + "full_name": repo.data.full_name, + "hide": repo.data.hide, + "hide_default_branch": repo.data.hide_default_branch, + "homeassistant": repo.data.homeassistant, + "id": repo.data.id, + "info": repo.information.info, + "installed_version": repo.display_installed_version, + "installed": repo.data.installed, + "issues": repo.data.open_issues, + "javascript_type": repo.information.javascript_type, + "last_updated": repo.data.last_updated, + "local_path": repo.content.path.local, + "main_action": repo.main_action, + "name": repo.display_name, + "new": repo.data.new, + "pending_upgrade": repo.pending_upgrade, + "releases": repo.data.published_tags, + "selected_tag": repo.data.selected_tag, + "stars": repo.data.stargazers_count, + "state": repo.state, + "status_description": repo.display_status_description, + "status": repo.display_status, + "topics": repo.data.topics, + "updated_info": repo.status.updated_info, + "version_or_commit": repo.display_version_or_commit, + } + + content.append(data) + + connection.send_message(websocket_api.result_message(msg["id"], content)) diff --git a/custom_components/hacs/api/hacs_repository.py b/custom_components/hacs/api/hacs_repository.py new file mode 100644 index 0000000..6921db4 --- /dev/null +++ b/custom_components/hacs/api/hacs_repository.py @@ -0,0 +1,113 @@ +"""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 + +from custom_components.hacs.helpers.functions.logger import getLogger +from custom_components.hacs.share import get_hacs + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository", + vol.Optional("action"): cv.string, + vol.Optional("repository"): cv.string, + } +) +async def hacs_repository(hass, connection, msg): + """Handle get media player cover command.""" + hacs = get_hacs() + logger = getLogger() + data = {} + repository = None + + repo_id = msg.get("repository") + action = msg.get("action") + if repo_id is None or action is None: + return + + try: + repository = hacs.get_by_id(repo_id) + logger.debug(f"Running {action} for {repository.data.full_name}") + + if action == "update": + await repository.update_repository(ignore_issues=True, force=True) + repository.status.updated_info = True + + elif action == "install": + repository.data.new = False + was_installed = repository.data.installed + await repository.async_install() + if not was_installed: + hass.bus.async_fire("hacs/reload", {"force": True}) + + elif action == "not_new": + repository.data.new = False + + elif action == "uninstall": + repository.data.new = False + await repository.update_repository(True) + await repository.uninstall() + + elif action == "hide": + repository.data.hide = True + + elif action == "unhide": + repository.data.hide = False + + elif action == "show_beta": + repository.data.show_beta = True + await repository.update_repository() + + elif action == "hide_beta": + repository.data.show_beta = False + await repository.update_repository() + + elif action == "toggle_beta": + repository.data.show_beta = not repository.data.show_beta + await repository.update_repository() + + elif action == "delete": + repository.data.show_beta = False + repository.remove() + + elif action == "release_notes": + data = [ + { + "name": x.attributes["name"], + "body": x.attributes["body"], + "tag": x.attributes["tag_name"], + } + for x in repository.releases.objects + ] + + elif action == "set_version": + if msg["version"] == repository.data.default_branch: + repository.data.selected_tag = None + else: + repository.data.selected_tag = msg["version"] + await repository.update_repository() + + hass.bus.async_fire("hacs/reload", {"force": True}) + + else: + logger.error(f"WS action '{action}' is not valid") + + await hacs.data.async_write() + message = None + except AIOGitHubAPIException as exception: + message = exception + except AttributeError as exception: + message = f"Could not use repository with ID {repo_id} ({exception})" + except (Exception, BaseException) as exception: # pylint: disable=broad-except + message = exception + + if message is not None: + logger.error(message) + hass.bus.async_fire("hacs/error", {"message": str(message)}) + + if repository: + repository.state = None + connection.send_message(websocket_api.result_message(msg["id"], data)) diff --git a/custom_components/hacs/api/hacs_repository_data.py b/custom_components/hacs/api/hacs_repository_data.py new file mode 100644 index 0000000..3052e72 --- /dev/null +++ b/custom_components/hacs/api/hacs_repository_data.py @@ -0,0 +1,121 @@ +"""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 + +from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.helpers.functions.logger import getLogger +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 + +_LOGGER = getLogger() + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/data", + vol.Optional("action"): cv.string, + vol.Optional("repository"): cv.string, + vol.Optional("data"): cv.string, + } +) +async def hacs_repository_data(hass, connection, msg): + """Handle get media player cover command.""" + hacs = get_hacs() + repo_id = msg.get("repository") + action = msg.get("action") + data = msg.get("data") + + if repo_id is None: + return + + if action == "add": + repo_id = extract_repository_from_url(repo_id) + if repo_id is None: + return + + if repo_id in hacs.common.skip: + hacs.common.skip.remove(repo_id) + + if not hacs.get_by_name(repo_id): + try: + registration = await register_repository(repo_id, data.lower()) + if registration is not None: + raise HacsException(registration) + except ( + Exception, + BaseException, + ) as exception: # pylint: disable=broad-except + hass.bus.async_fire( + "hacs/error", + { + "action": "add_repository", + "exception": str(sys.exc_info()[0].__name__), + "message": str(exception), + }, + ) + else: + hass.bus.async_fire( + "hacs/error", + { + "action": "add_repository", + "message": f"Repository '{repo_id}' exists in the store.", + }, + ) + + repository = hacs.get_by_name(repo_id) + else: + repository = hacs.get_by_id(repo_id) + + if repository is None: + hass.bus.async_fire("hacs/repository", {}) + return + + _LOGGER.debug("Running %s for %s", action, repository.data.full_name) + try: + if action == "set_state": + repository.state = data + + elif action == "set_version": + repository.data.selected_tag = data + await repository.update_repository() + + repository.state = None + + elif action == "install": + was_installed = repository.data.installed + repository.data.selected_tag = data + await repository.update_repository() + await repository.async_install() + repository.state = None + if not was_installed: + hass.bus.async_fire("hacs/reload", {"force": True}) + + elif action == "add": + repository.state = None + + else: + repository.state = None + _LOGGER.error("WS action '%s' is not valid", action) + + message = None + except AIOGitHubAPIException as exception: + message = exception + except AttributeError as exception: + message = f"Could not use repository with ID {repo_id} ({exception})" + except (Exception, BaseException) as exception: # pylint: disable=broad-except + message = exception + + if message is not None: + _LOGGER.error(message) + hass.bus.async_fire("hacs/error", {"message": str(message)}) + + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"], {})) diff --git a/custom_components/hacs/api/hacs_settings.py b/custom_components/hacs/api/hacs_settings.py new file mode 100644 index 0000000..fe07634 --- /dev/null +++ b/custom_components/hacs/api/hacs_settings.py @@ -0,0 +1,54 @@ +"""API Handler for hacs_settings""" +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 + +_LOGGER = getLogger() + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/settings", + vol.Optional("action"): cv.string, + vol.Optional("categories"): cv.ensure_list, + } +) +async def hacs_settings(hass, connection, msg): + """Handle get media player cover command.""" + hacs = get_hacs() + + action = msg["action"] + _LOGGER.debug("WS action '%s'", action) + + if action == "set_fe_grid": + hacs.configuration.frontend_mode = "Grid" + + elif action == "onboarding_done": + hacs.configuration.onboarding_done = True + + elif action == "set_fe_table": + hacs.configuration.frontend_mode = "Table" + + elif action == "set_fe_compact_true": + hacs.configuration.frontend_compact = False + + elif action == "set_fe_compact_false": + hacs.configuration.frontend_compact = True + + elif action == "clear_new": + for repo in hacs.repositories: + if repo.data.new and repo.data.category in msg.get("categories", []): + _LOGGER.debug( + "Clearing new flag from '%s'", + repo.data.full_name, + ) + repo.data.new = False + else: + _LOGGER.error("WS action '%s' is not valid", action) + hass.bus.async_fire("hacs/config", {}) + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"], {})) diff --git a/custom_components/hacs/api/hacs_status.py b/custom_components/hacs/api/hacs_status.py new file mode 100644 index 0000000..f840cc4 --- /dev/null +++ b/custom_components/hacs/api/hacs_status.py @@ -0,0 +1,24 @@ +"""API Handler for hacs_status""" +import voluptuous as vol +from homeassistant.components import websocket_api + +from custom_components.hacs.share import get_hacs + + +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "hacs/status"}) +async def hacs_status(_hass, connection, msg): + """Handle get media player cover command.""" + hacs = get_hacs() + content = { + "startup": hacs.status.startup, + "background_task": hacs.status.background_task, + "lovelace_mode": hacs.system.lovelace_mode, + "reloading_data": hacs.status.reloading_data, + "upgrading_all": hacs.status.upgrading_all, + "disabled": hacs.system.disabled, + "disabled_reason": hacs.system.disabled_reason, + "has_pending_tasks": hacs.queue.has_pending_tasks, + "stage": hacs.stage, + } + connection.send_message(websocket_api.result_message(msg["id"], content)) diff --git a/custom_components/hacs/base.py b/custom_components/hacs/base.py new file mode 100644 index 0000000..8e91ea3 --- /dev/null +++ b/custom_components/hacs/base.py @@ -0,0 +1,141 @@ +"""Base HACS class.""" +import logging +from typing import List, Optional, TYPE_CHECKING +import pathlib + +import attr +from aiogithubapi.github import AIOGitHubAPI +from aiogithubapi.objects.repository import AIOGitHubAPIRepository +from homeassistant.core import HomeAssistant + +from .enums import HacsDisabledReason, HacsStage +from .helpers.functions.logger import getLogger +from .hacsbase.configuration import Configuration +from .models.core import HacsCore +from .models.frontend import HacsFrontend +from .models.system import HacsSystem + +if TYPE_CHECKING: + from .helpers.classes.repository import HacsRepository + + +class HacsCommon: + """Common for HACS.""" + + categories: List = [] + default: List = [] + installed: List = [] + skip: List = [] + + +class HacsStatus: + """HacsStatus.""" + + startup: bool = True + new: bool = False + background_task: bool = False + reloading_data: bool = False + upgrading_all: bool = False + + +@attr.s +class HacsBaseAttributes: + """Base HACS class.""" + + _default: Optional[AIOGitHubAPIRepository] + _github: Optional[AIOGitHubAPI] + _hass: Optional[HomeAssistant] + _configuration: Optional[Configuration] + _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"] = [] + + +@attr.s +class HacsBase(HacsBaseAttributes): + """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 + + @property + def configuration(self) -> Optional[Configuration]: + """Returns a Configuration object.""" + return self._configuration + + @configuration.setter + def configuration(self, value: Configuration) -> None: + """Set the value for the default property.""" + self._configuration = value + + @property + def integration_dir(self) -> pathlib.Path: + """Return the HACS integration dir.""" + return pathlib.Path(__file__).parent + + def disable(self, reason: HacsDisabledReason) -> None: + """Disable HACS.""" + self.system.disabled = True + self.system.disabled_reason = reason + self.log.error("HACS is disabled - %s", reason) + + def enable(self) -> None: + """Enable HACS.""" + self.system.disabled = False + self.system.disabled_reason = None + self.log.info("HACS is enabled") diff --git a/custom_components/hacs/config_flow.py b/custom_components/hacs/config_flow.py index 7bf329d..f90d5ea 100644 --- a/custom_components/hacs/config_flow.py +++ b/custom_components/hacs/config_flow.py @@ -1,102 +1,160 @@ -"""Adds config flow for HACS.""" -# pylint: disable=dangerous-default-value -import logging -import voluptuous as vol -from aiogithubapi import AIOGitHubException, AIOGitHubAuthentication -from homeassistant import config_entries -from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client - -from .const import DOMAIN -from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema - -from custom_components.hacs.globals import get_hacs -from custom_components.hacs.helpers.information import get_repository - -_LOGGER = logging.getLogger(__name__) - - -class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Config flow for HACS.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - - def __init__(self): - """Initialize.""" - self._errors = {} - - async def async_step_user(self, user_input={}): - """Handle a flow initialized by the user.""" - self._errors = {} - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if self.hass.data.get(DOMAIN): - return self.async_abort(reason="single_instance_allowed") - - if user_input is not None: - if await self._test_token(user_input["token"]): - return self.async_create_entry(title="", data=user_input) - - self._errors["base"] = "auth" - return await self._show_config_form(user_input) - - return await self._show_config_form(user_input) - - async def _show_config_form(self, user_input): - """Show the configuration form to edit location data.""" - return self.async_show_form( - step_id="user", - data_schema=vol.Schema(hacs_base_config_schema(user_input, True)), - errors=self._errors, - ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - return HacsOptionsFlowHandler(config_entry) - - async def async_step_import(self, user_input): - """Import a config entry. - Special type of import, we're not actually going to store any data. - Instead, we're going to rely on the values that are in config file. - """ - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return self.async_create_entry(title="configuration.yaml", data={}) - - async def _test_token(self, token): - """Return true if token is valid.""" - try: - session = aiohttp_client.async_get_clientsession(self.hass) - await get_repository(session, token, "hacs/org") - return True - except (AIOGitHubException, AIOGitHubAuthentication) as exception: - _LOGGER.error(exception) - return False - - -class HacsOptionsFlowHandler(config_entries.OptionsFlow): - """HACS config flow options handler.""" - - def __init__(self, config_entry): - """Initialize HACS options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Manage the options.""" - return await self.async_step_user() - - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - hacs = get_hacs() - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - if hacs.configuration.config_type == "yaml": - schema = {vol.Optional("not_in_use", default=""): str} - else: - schema = hacs_config_option_schema(self.config_entry.options) - - return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) +"""Adds config flow for HACS.""" +import voluptuous as vol +from aiogithubapi import AIOGitHubAPIException, GitHubDevice +from aiogithubapi.common.const import OAUTH_USER_LOGIN +from awesomeversion import AwesomeVersion +from homeassistant import config_entries +from homeassistant.const import __version__ as HAVERSION +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.event import async_call_later + +from custom_components.hacs.const import CLIENT_ID, DOMAIN, MINIMUM_HA_VERSION +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 + +_LOGGER = getLogger() + + +class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for HACS.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize.""" + self._errors = {} + self.device = None + self.activation = None + self._progress_task = None + + async def async_step_user(self, user_input): + """Handle a flow initialized by the user.""" + self._errors = {} + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if self.hass.data.get(DOMAIN): + return self.async_abort(reason="single_instance_allowed") + + if user_input: + if [x for x in user_input if not user_input[x]]: + self._errors["base"] = "acc" + return await self._show_config_form(user_input) + + return await self.async_step_device(user_input) + + ## Initial form + return await self._show_config_form(user_input) + + 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() + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + ) + + if not self.activation: + if not self.device: + self.device = GitHubDevice( + CLIENT_ID, + session=aiohttp_client.async_get_clientsession(self.hass), + ) + async_call_later(self.hass, 1, _wait_for_activation) + try: + device_data = await self.device.async_register_device() + return self.async_show_progress( + step_id="device", + progress_action="wait_for_device", + description_placeholders={ + "url": OAUTH_USER_LOGIN, + "code": device_data.user_code, + }, + ) + except AIOGitHubAPIException as exception: + _LOGGER.error(exception) + return self.async_abort(reason="github") + + return self.async_show_progress_done(next_step_id="device_done") + + async def _show_config_form(self, user_input): + """Show the configuration form to edit location data.""" + if not user_input: + user_input = {} + if AwesomeVersion(HAVERSION) < MINIMUM_HA_VERSION: + return self.async_abort( + reason="min_ha_version", + description_placeholders={"version": MINIMUM_HA_VERSION}, + ) + return self.async_show_form( + 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_untested", default=user_input.get("acc_untested", False) + ): bool, + vol.Required( + "acc_disable", default=user_input.get("acc_disable", False) + ): bool, + } + ), + errors=self._errors, + ) + + async def async_step_device_done(self, _user_input): + """Handle device steps""" + return self.async_create_entry( + title="", data={"token": self.activation.access_token} + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + return HacsOptionsFlowHandler(config_entry) + + +class HacsOptionsFlowHandler(config_entries.OptionsFlow): + """HACS config flow options handler.""" + + def __init__(self, config_entry): + """Initialize HACS options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, _user_input=None): + """Manage the options.""" + return await self.async_step_user() + + 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: + return self.async_abort(reason="not_setup") + + if hacs.configuration.config_type == "yaml": + schema = {vol.Optional("not_in_use", default=""): str} + else: + schema = hacs_config_option_schema(self.config_entry.options) + del schema["frontend_repo"] + del schema["frontend_repo_url"] + + return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) diff --git a/custom_components/hacs/const.py b/custom_components/hacs/const.py index 47f44f2..c3e6983 100644 --- a/custom_components/hacs/const.py +++ b/custom_components/hacs/const.py @@ -1,293 +1,313 @@ -"""Constants for HACS""" -NAME_LONG = "HACS (Home Assistant Community Store)" -NAME_SHORT = "HACS" -VERSION = "0.23.2" -DOMAIN = "hacs" -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"] - -IFRAME = { - "title": "Community", - "icon": "mdi:alpha-c-box", - "url": "/community_overview", - "path": "community", - "require_admin": True, -} - - -# Messages -CUSTOM_UPDATER_WARNING = """ -This cannot be used with custom_updater. -To use this you need to remove custom_updater form {} -""" - -STARTUP = f""" -------------------------------------------------------------------- -HACS (Home Assistant Community Store) - -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 -------------------------------------------------------------------- -""" - -LOCALE = [ - "ALL", - "AF", - "AL", - "DZ", - "AS", - "AD", - "AO", - "AI", - "AQ", - "AG", - "AR", - "AM", - "AW", - "AU", - "AT", - "AZ", - "BS", - "BH", - "BD", - "BB", - "BY", - "BE", - "BZ", - "BJ", - "BM", - "BT", - "BO", - "BQ", - "BA", - "BW", - "BV", - "BR", - "IO", - "BN", - "BG", - "BF", - "BI", - "KH", - "CM", - "CA", - "CV", - "KY", - "CF", - "TD", - "CL", - "CN", - "CX", - "CC", - "CO", - "KM", - "CG", - "CD", - "CK", - "CR", - "HR", - "CU", - "CW", - "CY", - "CZ", - "CI", - "DK", - "DJ", - "DM", - "DO", - "EC", - "EG", - "SV", - "GQ", - "ER", - "EE", - "ET", - "FK", - "FO", - "FJ", - "FI", - "FR", - "GF", - "PF", - "TF", - "GA", - "GM", - "GE", - "DE", - "GH", - "GI", - "GR", - "GL", - "GD", - "GP", - "GU", - "GT", - "GG", - "GN", - "GW", - "GY", - "HT", - "HM", - "VA", - "HN", - "HK", - "HU", - "IS", - "IN", - "ID", - "IR", - "IQ", - "IE", - "IM", - "IL", - "IT", - "JM", - "JP", - "JE", - "JO", - "KZ", - "KE", - "KI", - "KP", - "KR", - "KW", - "KG", - "LA", - "LV", - "LB", - "LS", - "LR", - "LY", - "LI", - "LT", - "LU", - "MO", - "MK", - "MG", - "MW", - "MY", - "MV", - "ML", - "MT", - "MH", - "MQ", - "MR", - "MU", - "YT", - "MX", - "FM", - "MD", - "MC", - "MN", - "ME", - "MS", - "MA", - "MZ", - "MM", - "NA", - "NR", - "NP", - "NL", - "NC", - "NZ", - "NI", - "NE", - "NG", - "NU", - "NF", - "MP", - "NO", - "OM", - "PK", - "PW", - "PS", - "PA", - "PG", - "PY", - "PE", - "PH", - "PN", - "PL", - "PT", - "PR", - "QA", - "RO", - "RU", - "RW", - "RE", - "BL", - "SH", - "KN", - "LC", - "MF", - "PM", - "VC", - "WS", - "SM", - "ST", - "SA", - "SN", - "RS", - "SC", - "SL", - "SG", - "SX", - "SK", - "SI", - "SB", - "SO", - "ZA", - "GS", - "SS", - "ES", - "LK", - "SD", - "SR", - "SJ", - "SZ", - "SE", - "CH", - "SY", - "TW", - "TJ", - "TZ", - "TH", - "TL", - "TG", - "TK", - "TO", - "TT", - "TN", - "TR", - "TM", - "TC", - "TV", - "UG", - "UA", - "AE", - "GB", - "US", - "UM", - "UY", - "UZ", - "VU", - "VE", - "VN", - "VG", - "VI", - "WF", - "EH", - "YE", - "ZM", - "ZW", -] +"""Constants for HACS""" +from aiogithubapi.common.const import ACCEPT_HEADERS + +NAME_LONG = "HACS (Home Assistant Community Store)" +NAME_SHORT = "HACS" +INTEGRATION_VERSION = "1.13.2" +DOMAIN = "hacs" +CLIENT_ID = "395a8e669c5de9f7c6e8" +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"], +} + +HACS_ACTION_GITHUB_API_HEADERS = { + "User-Agent": "HACS/action", + "Accept": ACCEPT_HEADERS["preview"], +} + +IFRAME = { + "title": "HACS", + "icon": "hacs:hacs", + "url": "/community_overview", + "path": "community", + "require_admin": True, +} + +VERSION_STORAGE = "6" +STORENAME = "hacs" + +# 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""" +------------------------------------------------------------------- +HACS (Home Assistant Community Store) + +Version: {INTEGRATION_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 +------------------------------------------------------------------- +""" + +LOCALE = [ + "ALL", + "AF", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AQ", + "AG", + "AR", + "AM", + "AW", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BB", + "BY", + "BE", + "BZ", + "BJ", + "BM", + "BT", + "BO", + "BQ", + "BA", + "BW", + "BV", + "BR", + "IO", + "BN", + "BG", + "BF", + "BI", + "KH", + "CM", + "CA", + "CV", + "KY", + "CF", + "TD", + "CL", + "CN", + "CX", + "CC", + "CO", + "KM", + "CG", + "CD", + "CK", + "CR", + "HR", + "CU", + "CW", + "CY", + "CZ", + "CI", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "GQ", + "ER", + "EE", + "ET", + "FK", + "FO", + "FJ", + "FI", + "FR", + "GF", + "PF", + "TF", + "GA", + "GM", + "GE", + "DE", + "GH", + "GI", + "GR", + "GL", + "GD", + "GP", + "GU", + "GT", + "GG", + "GN", + "GW", + "GY", + "HT", + "HM", + "VA", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IR", + "IQ", + "IE", + "IM", + "IL", + "IT", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KP", + "KR", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LR", + "LY", + "LI", + "LT", + "LU", + "MO", + "MK", + "MG", + "MW", + "MY", + "MV", + "ML", + "MT", + "MH", + "MQ", + "MR", + "MU", + "YT", + "MX", + "FM", + "MD", + "MC", + "MN", + "ME", + "MS", + "MA", + "MZ", + "MM", + "NA", + "NR", + "NP", + "NL", + "NC", + "NZ", + "NI", + "NE", + "NG", + "NU", + "NF", + "MP", + "NO", + "OM", + "PK", + "PW", + "PS", + "PA", + "PG", + "PY", + "PE", + "PH", + "PN", + "PL", + "PT", + "PR", + "QA", + "RO", + "RU", + "RW", + "RE", + "BL", + "SH", + "KN", + "LC", + "MF", + "PM", + "VC", + "WS", + "SM", + "ST", + "SA", + "SN", + "RS", + "SC", + "SL", + "SG", + "SX", + "SK", + "SI", + "SB", + "SO", + "ZA", + "GS", + "SS", + "ES", + "LK", + "SD", + "SR", + "SJ", + "SZ", + "SE", + "CH", + "SY", + "TW", + "TJ", + "TZ", + "TH", + "TL", + "TG", + "TK", + "TO", + "TT", + "TN", + "TR", + "TM", + "TC", + "TV", + "UG", + "UA", + "AE", + "GB", + "US", + "UM", + "UY", + "UZ", + "VU", + "VE", + "VN", + "VG", + "VI", + "WF", + "EH", + "YE", + "ZM", + "ZW", +] diff --git a/custom_components/hacs/constrains.py b/custom_components/hacs/constrains.py deleted file mode 100644 index 269cba8..0000000 --- a/custom_components/hacs/constrains.py +++ /dev/null @@ -1,99 +0,0 @@ -"""HACS Startup constrains.""" -# pylint: disable=bad-continuation -import os - -from .const import CUSTOM_UPDATER_LOCATIONS, CUSTOM_UPDATER_WARNING -from .helpers.misc import version_left_higher_then_right - -from custom_components.hacs.globals import get_hacs - -MINIMUM_HA_VERSION = "0.98.0" - - -def check_constans(): - """Check HACS constrains.""" - if not constrain_translations(): - return False - 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.system.config_path)): - msg = CUSTOM_UPDATER_WARNING.format( - location.format(hacs.system.config_path) - ) - hacs.logger.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.logger.critical( - f"You need HA version {MINIMUM_HA_VERSION} or newer to use this integration." - ) - return False - return True - - -def constrain_translations(): - """Check if traslations exist.""" - hacs = get_hacs() - if not os.path.exists( - f"{hacs.system.config_path}/custom_components/hacs/.translations" - ): - hacs.logger.critical("You are missing the translations directory.") - return False - return True - - -def check_requirements(): - """Check the requirements""" - missing = [] - try: - from aiogithubapi import AIOGitHubException # pylint: disable=unused-import - except ImportError: - missing.append("aiogithubapi") - - try: - from hacs_frontend import locate_gz # pylint: disable=unused-import - except ImportError: - missing.append("hacs_frontend") - - try: - import semantic_version # pylint: disable=unused-import - except ImportError: - missing.append("semantic_version") - - try: - from integrationhelper import Logger # pylint: disable=unused-import - except ImportError: - missing.append("integrationhelper") - - try: - import backoff # pylint: disable=unused-import - except ImportError: - missing.append("backoff") - - try: - import aiofiles # pylint: disable=unused-import - except ImportError: - missing.append("aiofiles") - - if missing: - hacs = get_hacs() - for requirement in missing: - hacs.logger.critical( - f"Required python requirement '{requirement}' is not installed" - ) - return False - return True diff --git a/custom_components/hacs/enums.py b/custom_components/hacs/enums.py new file mode 100644 index 0000000..af8a6a0 --- /dev/null +++ b/custom_components/hacs/enums.py @@ -0,0 +1,49 @@ +"""Helper constants.""" +# pylint: disable=missing-class-docstring +from enum import Enum + + +class HacsCategory(str, Enum): + APPDAEMON = "appdaemon" + INTEGRATION = "integration" + LOVELACE = "lovelace" + PLUGIN = "plugin" # Kept for legacy purposes + NETDAEMON = "netdaemon" + PYTHON_SCRIPT = "python_script" + THEME = "theme" + REMOVED = "removed" + + +class LovelaceMode(str, Enum): + """Lovelace Modes.""" + + STORAGE = "storage" + AUTO = "auto" + AUTO_GEN = "auto-gen" + YAML = "yaml" + + +class HacsStage(str, Enum): + SETUP = "setup" + STARTUP = "startup" + WAITING = "waiting" + RUNNING = "running" + BACKGROUND = "background" + + +class HacsSetupTask(str, Enum): + WEBSOCKET = "WebSocket API" + FRONTEND = "Frontend" + SENSOR = "Sensor" + HACS_REPO = "Hacs Repository" + CATEGORIES = "Additional categories" + CLEAR_STORAGE = "Clear storage" + + +class HacsDisabledReason(str, Enum): + RATE_LIMIT = "rate_limit" + REMOVED = "removed" + INVALID_TOKEN = "invalid_token" + CONSTRAINS = "constrains" + LOAD_HACS = "load_hacs" + RESTORE = "restore" diff --git a/custom_components/hacs/globals.py b/custom_components/hacs/globals.py deleted file mode 100644 index 91d8314..0000000 --- a/custom_components/hacs/globals.py +++ /dev/null @@ -1,29 +0,0 @@ -# pylint: disable=invalid-name, missing-docstring -hacs = [] -removed_repositories = [] - - -def get_hacs(): - if not hacs: - from custom_components.hacs.hacsbase import Hacs - - hacs.append(Hacs()) - - return hacs[0] - - -def is_removed(repository): - return repository in [x.repository for x in removed_repositories] - - -def get_removed(repository): - if not is_removed(repository): - from custom_components.hacs.repositories.removed import RemovedRepository - - removed_repo = RemovedRepository() - removed_repo.repository = repository - removed_repositories.append(removed_repo) - filter_repos = [ - x for x in removed_repositories if x.repository.lower() == repository.lower() - ] - return filter_repos[0] diff --git a/custom_components/hacs/hacsbase/__init__.py b/custom_components/hacs/hacsbase/__init__.py index 8795661..e69de29 100644 --- a/custom_components/hacs/hacsbase/__init__.py +++ b/custom_components/hacs/hacsbase/__init__.py @@ -1,355 +0,0 @@ -"""Initialize the HACS base.""" -# pylint: disable=unused-argument, bad-continuation -import json -import uuid -from datetime import timedelta - -from homeassistant.helpers.event import async_call_later, async_track_time_interval - -from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit -from integrationhelper import Logger - -from custom_components.hacs.hacsbase.task_factory import HacsTaskFactory -from custom_components.hacs.hacsbase.exceptions import HacsException - -from custom_components.hacs.const import ELEMENT_TYPES -from custom_components.hacs.setup import setup_extra_stores -from custom_components.hacs.store import async_load_from_store, async_save_to_store -from custom_components.hacs.helpers.get_defaults import ( - get_default_repos_lists, - get_default_repos_orgs, -) - -from custom_components.hacs.helpers.register_repository import register_repository -from custom_components.hacs.globals import removed_repositories, get_removed, is_removed -from custom_components.hacs.repositories.removed import RemovedRepository - - -class HacsStatus: - """HacsStatus.""" - - startup = True - new = False - background_task = False - reloading_data = False - upgrading_all = False - - -class HacsFrontend: - """HacsFrontend.""" - - version_running = None - version_available = 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 - lovelace_mode = "storage" - - -class Developer: - """Developer settings/tools.""" - - template_id = "Repository ID" - template_content = "" - template_raw = "" - - @property - def devcontainer(self): - """Is it a devcontainer?""" - import os - - if "DEVCONTAINER" in os.environ: - return True - return False - - -class Hacs: - """The base class of HACS, nested thoughout the project.""" - - token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}" - hacsweb = f"/hacsweb/{token}" - hacsapi = f"/hacsapi/{token}" - repositories = [] - frontend = HacsFrontend() - repo = None - data_repo = None - developer = Developer() - data = None - configuration = None - logger = Logger("hacs") - github = None - hass = None - version = None - session = None - factory = HacsTaskFactory() - system = System() - recuring_tasks = [] - common = HacsCommon() - - @staticmethod - def init(hass, github_token): - """Return a initialized HACS object.""" - return Hacs() - - def get_by_id(self, repository_id): - """Get repository by ID.""" - try: - for repository in self.repositories: - if repository.information.uid == repository_id: - return repository - except Exception: # pylint: disable=broad-except - pass - return None - - def get_by_name(self, repository_full_name): - """Get repository by full_name.""" - try: - for repository in self.repositories: - if repository.data.full_name.lower() == repository_full_name.lower(): - return repository - except Exception: # pylint: disable=broad-except - pass - return None - - def is_known(self, repository_full_name): - """Return a bool if the repository is known.""" - return repository_full_name.lower() in [ - x.data.full_name.lower() for x in self.repositories - ] - - @property - def sorted_by_name(self): - """Return a sorted(by name) list of repository objects.""" - return sorted(self.repositories, key=lambda x: x.display_name) - - @property - def sorted_by_repository_name(self): - """Return a sorted(by repository_name) list of repository objects.""" - return sorted(self.repositories, key=lambda x: x.data.full_name) - - async def register_repository(self, full_name, category, check=True): - """Register a repository.""" - await register_repository(full_name, category, check=True) - - async def startup_tasks(self): - """Tasks tha are started after startup.""" - self.system.status.background_task = True - await self.hass.async_add_executor_job(setup_extra_stores) - self.hass.bus.async_fire("hacs/status", {}) - self.logger.debug(self.github.ratelimits.remaining) - self.logger.debug(self.github.ratelimits.reset_utc) - - await self.handle_critical_repositories_startup() - await self.handle_critical_repositories() - await self.load_known_repositories() - await self.clear_out_removed_repositories() - - self.recuring_tasks.append( - async_track_time_interval( - self.hass, self.recuring_tasks_installed, timedelta(minutes=30) - ) - ) - self.recuring_tasks.append( - async_track_time_interval( - self.hass, self.recuring_tasks_all, timedelta(minutes=800) - ) - ) - - self.hass.bus.async_fire("hacs/reload", {"force": True}) - await self.recuring_tasks_installed() - - self.system.status.startup = False - self.system.status.new = False - self.system.status.background_task = False - self.hass.bus.async_fire("hacs/status", {}) - await self.data.async_write() - - async def handle_critical_repositories_startup(self): - """Handled critical repositories during startup.""" - alert = False - critical = await async_load_from_store(self.hass, "critical") - if not critical: - return - for repo in critical: - if not repo["acknowledged"]: - alert = True - if alert: - self.logger.critical("URGENT!: Check the HACS panel!") - self.hass.components.persistent_notification.create( - title="URGENT!", message="**Check the HACS panel!**" - ) - - async def handle_critical_repositories(self): - """Handled critical repositories during runtime.""" - # Get critical repositories - instored = [] - critical = [] - was_installed = False - - try: - critical = await self.data_repo.get_contents("critical") - critical = json.loads(critical.content) - except AIOGitHubException: - pass - - if not critical: - self.logger.debug("No critical repositories") - return - - stored_critical = await async_load_from_store(self.hass, "critical") - - for stored in stored_critical or []: - instored.append(stored["repository"]) - - stored_critical = [] - - for repository in critical: - removed_repo = get_removed(repository["repository"]) - removed_repo.removal_type = "critical" - repo = self.get_by_name(repository["repository"]) - - stored = { - "repository": repository["repository"], - "reason": repository["reason"], - "link": repository["link"], - "acknowledged": True, - } - if repository["repository"] not in instored: - if repo is not None and repo.installed: - self.logger.critical( - f"Removing repository {repository['repository']}, it is marked as critical" - ) - was_installed = True - stored["acknowledged"] = False - # Uninstall from HACS - repo.remove() - await repo.uninstall() - stored_critical.append(stored) - removed_repo.update_data(stored) - - # Save to FS - await async_save_to_store(self.hass, "critical", stored_critical) - - # Resart HASS - if was_installed: - self.logger.critical("Resarting Home Assistant") - self.hass.async_create_task(self.hass.async_stop(100)) - - async def recuring_tasks_installed(self, notarealarg=None): - """Recuring tasks for installed repositories.""" - self.logger.debug( - "Starting recuring background task for installed repositories" - ) - self.system.status.background_task = True - self.hass.bus.async_fire("hacs/status", {}) - self.logger.debug(self.github.ratelimits.remaining) - self.logger.debug(self.github.ratelimits.reset_utc) - for repository in self.repositories: - if ( - repository.status.installed - and repository.data.category in self.common.categories - ): - self.factory.tasks.append(self.factory.safe_update(repository)) - - await self.factory.execute() - await self.handle_critical_repositories() - self.system.status.background_task = False - self.hass.bus.async_fire("hacs/status", {}) - await self.data.async_write() - self.logger.debug("Recuring background task for installed repositories done") - - async def recuring_tasks_all(self, notarealarg=None): - """Recuring tasks for all repositories.""" - self.logger.debug("Starting recuring background task for all repositories") - await self.hass.async_add_executor_job(setup_extra_stores) - self.system.status.background_task = True - self.hass.bus.async_fire("hacs/status", {}) - self.logger.debug(self.github.ratelimits.remaining) - self.logger.debug(self.github.ratelimits.reset_utc) - for repository in self.repositories: - if repository.data.category in self.common.categories: - self.factory.tasks.append(self.factory.safe_common_update(repository)) - - await self.factory.execute() - await self.load_known_repositories() - await self.clear_out_removed_repositories() - self.system.status.background_task = False - await self.data.async_write() - self.hass.bus.async_fire("hacs/status", {}) - self.hass.bus.async_fire("hacs/repository", {"action": "reload"}) - self.logger.debug("Recuring background task for all repositories done") - - async def clear_out_removed_repositories(self): - """Clear out blaclisted repositories.""" - need_to_save = False - for removed in removed_repositories: - if self.is_known(removed.repository): - repository = self.get_by_name(removed.repository) - if repository.status.installed and removed.removal_type != "critical": - self.logger.warning( - f"You have {repository.data.full_name} installed with HACS " - + f"this repository has been removed, please consider removing it. " - + f"Removal reason ({removed.removal_type})" - ) - else: - need_to_save = True - repository.remove() - - if need_to_save: - await self.data.async_write() - - async def get_repositories(self): - """Return a list of repositories.""" - repositories = {} - for category in self.common.categories: - repositories[category] = await get_default_repos_lists( - self.session, self.configuration.token, category - ) - org = await get_default_repos_orgs(self.github, category) - for repo in org: - repositories[category].append(repo) - - for category in repositories: - for repo in repositories[category]: - if repo not in self.common.default: - self.common.default.append(repo) - return repositories - - async def load_known_repositories(self): - """Load known repositories.""" - self.logger.info("Loading known repositories") - repositories = await self.get_repositories() - - for item in await get_default_repos_lists( - self.session, self.configuration.token, "removed" - ): - removed = get_removed(item["repository"]) - removed.reason = item.get("reason") - removed.link = item.get("link") - removed.removal_type = item.get("removal_type") - - for category in repositories: - for repo in repositories[category]: - if is_removed(repo): - continue - if self.is_known(repo): - continue - self.factory.tasks.append(self.factory.safe_register(repo, category)) - await self.factory.execute() - self.logger.info("Loading known repositories finished") diff --git a/custom_components/hacs/hacsbase/__pycache__/__init__.cpython-37.pyc b/custom_components/hacs/hacsbase/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index b03ea9d..0000000 Binary files a/custom_components/hacs/hacsbase/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/hacsbase/__pycache__/backup.cpython-37.pyc b/custom_components/hacs/hacsbase/__pycache__/backup.cpython-37.pyc deleted file mode 100644 index 38e8774..0000000 Binary files a/custom_components/hacs/hacsbase/__pycache__/backup.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/hacsbase/__pycache__/configuration.cpython-37.pyc b/custom_components/hacs/hacsbase/__pycache__/configuration.cpython-37.pyc deleted file mode 100644 index 89c0712..0000000 Binary files a/custom_components/hacs/hacsbase/__pycache__/configuration.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/hacsbase/__pycache__/const.cpython-37.pyc b/custom_components/hacs/hacsbase/__pycache__/const.cpython-37.pyc deleted file mode 100644 index 3d42257..0000000 Binary files a/custom_components/hacs/hacsbase/__pycache__/const.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/hacsbase/__pycache__/data.cpython-37.pyc b/custom_components/hacs/hacsbase/__pycache__/data.cpython-37.pyc deleted file mode 100644 index 4f51ff0..0000000 Binary files a/custom_components/hacs/hacsbase/__pycache__/data.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/hacsbase/__pycache__/exceptions.cpython-37.pyc b/custom_components/hacs/hacsbase/__pycache__/exceptions.cpython-37.pyc deleted file mode 100644 index 14590ec..0000000 Binary files a/custom_components/hacs/hacsbase/__pycache__/exceptions.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/hacsbase/__pycache__/task_factory.cpython-37.pyc b/custom_components/hacs/hacsbase/__pycache__/task_factory.cpython-37.pyc deleted file mode 100644 index c48f246..0000000 Binary files a/custom_components/hacs/hacsbase/__pycache__/task_factory.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/hacsbase/configuration.py b/custom_components/hacs/hacsbase/configuration.py index c9bb80e..825582b 100644 --- a/custom_components/hacs/hacsbase/configuration.py +++ b/custom_components/hacs/hacsbase/configuration.py @@ -1,73 +1,77 @@ -"""HACS Configuration.""" -import attr -from integrationhelper import Logger -from custom_components.hacs.hacsbase.exceptions import HacsException - - -@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 - options: dict = {} - onboarding_done: bool = False - plugin_path: str = "www/community/" - python_script_path: str = "python_scripts/" - python_script: bool = False - sidepanel_icon: str = "mdi:alpha-c-box" - sidepanel_title: str = "Community" - 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): - """Return a dict representation of the configuration.""" - return self.__dict__ - - def print(self): - """Print the current configuration to the log.""" - logger = Logger("hacs.configuration") - config = self.to_json() - for key in config: - if key in ["config", "config_entry", "options", "token"]: - continue - logger.debug(f"{key}: {config[key]}") - - @staticmethod - def from_dict(configuration: dict, options: dict): - """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 +"""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/const.py b/custom_components/hacs/hacsbase/const.py deleted file mode 100644 index 1e72154..0000000 --- a/custom_components/hacs/hacsbase/const.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Constants for HACS""" -# pylint: disable=unused-import -STORAGE_VERSION = "6" -STORENAME = "hacs" - -# Messages -NOT_SUPPORTED_HA_VERSION = "You have version '{}' of Home Assistant, but version '{}' of '{}' require version '{}' of Home Assistant, install and upgrades are disabled for this integration untill you upgrade Home Assistant." - - -NO_ELEMENTS = "No elements to show, open the store to install some awesome stuff." diff --git a/custom_components/hacs/hacsbase/data.py b/custom_components/hacs/hacsbase/data.py index 1464849..d159fb0 100644 --- a/custom_components/hacs/hacsbase/data.py +++ b/custom_components/hacs/hacsbase/data.py @@ -1,152 +1,209 @@ -"""Data handler for HACS.""" -from integrationhelper import Logger -from ..const import VERSION -from ..repositories.repository import HacsRepository -from ..repositories.manifest import HacsManifest -from ..store import async_save_to_store, async_load_from_store - -from custom_components.hacs.globals import get_hacs, removed_repositories, get_removed -from custom_components.hacs.helpers.register_repository import register_repository - - -class HacsData: - """HacsData class.""" - - def __init__(self): - """Initialize.""" - self.logger = Logger("hacs.data") - self.hacs = get_hacs() - - async def async_write(self): - """Write content to the store files.""" - if self.hacs.system.status.background_task or self.hacs.system.disabled: - return - - self.logger.debug("Saving data") - - # Hacs - await async_save_to_store( - self.hacs.hass, - "hacs", - { - "view": self.hacs.configuration.frontend_mode, - "compact": self.hacs.configuration.frontend_compact, - "onboarding_done": self.hacs.configuration.onboarding_done, - }, - ) - - await async_save_to_store( - self.hacs.hass, "removed", [x.__dict__ for x in removed_repositories] - ) - - # Repositories - content = {} - for repository in self.hacs.repositories: - if repository.repository_manifest is not None: - repository_manifest = repository.repository_manifest.manifest - else: - repository_manifest = None - content[repository.information.uid] = { - "authors": repository.data.authors, - "category": repository.data.category, - "description": repository.data.description, - "downloads": repository.releases.downloads, - "full_name": repository.data.full_name, - "first_install": repository.status.first_install, - "hide": repository.status.hide, - "installed_commit": repository.versions.installed_commit, - "installed": repository.status.installed, - "last_commit": repository.versions.available_commit, - "last_release_tag": repository.versions.available, - "last_updated": repository.information.last_updated, - "name": repository.data.name, - "new": repository.status.new, - "repository_manifest": repository_manifest, - "selected_tag": repository.status.selected_tag, - "show_beta": repository.status.show_beta, - "stars": repository.data.stargazers_count, - "topics": repository.data.topics, - "version_installed": repository.versions.installed, - } - - await async_save_to_store(self.hacs.hass, "repositories", content) - self.hacs.hass.bus.async_fire("hacs/repository", {}) - self.hacs.hass.bus.fire("hacs/config", {}) - - async def restore(self): - """Restore saved data.""" - hacs = await async_load_from_store(self.hacs.hass, "hacs") - repositories = await async_load_from_store(self.hacs.hass, "repositories") - removed = await async_load_from_store(self.hacs.hass, "removed") - try: - if not hacs and not repositories: - # Assume new install - self.hacs.system.status.new = True - return True - self.logger.info("Restore started") - - # 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) - - for entry in removed: - removed_repo = get_removed(entry["repository"]) - removed_repo.update_data(entry) - - # Repositories - for entry in repositories: - repo = repositories[entry] - if not self.hacs.is_known(repo["full_name"]): - await register_repository( - repo["full_name"], repo["category"], False - ) - repository = self.hacs.get_by_name(repo["full_name"]) - if repository is None: - self.logger.error(f"Did not find {repo['full_name']}") - continue - - # Restore repository attributes - repository.information.uid = entry - await self.hacs.hass.async_add_executor_job( - restore_repository_data, repository, repo - ) - - self.logger.info("Restore done") - except Exception as exception: # pylint: disable=broad-except - self.logger.critical(f"[{exception}] Restore Failed!") - return False - return True - - -def restore_repository_data( - repository: type(HacsRepository), repository_data: dict -) -> None: - """Restore Repository Data""" - repository.data.authors = repository_data.get("authors", []) - repository.data.description = repository_data.get("description") - repository.releases.last_release_object_downloads = repository_data.get("downloads") - repository.information.last_updated = repository_data.get("last_updated") - repository.data.topics = repository_data.get("topics", []) - repository.data.stargazers_count = repository_data.get("stars", 0) - repository.releases.last_release = repository_data.get("last_release_tag") - repository.status.hide = repository_data.get("hide", False) - repository.status.installed = repository_data.get("installed", False) - repository.status.new = repository_data.get("new", True) - repository.status.selected_tag = repository_data.get("selected_tag") - repository.status.show_beta = repository_data.get("show_beta", False) - repository.versions.available = repository_data.get("last_release_tag") - repository.versions.available_commit = repository_data.get("last_commit") - repository.versions.installed = repository_data.get("version_installed") - repository.versions.installed_commit = repository_data.get("installed_commit") - - repository.repository_manifest = HacsManifest.from_dict( - repository_data.get("repository_manifest", {}) - ) - - if repository.status.installed: - repository.status.first_install = False - - if repository_data["full_name"] == "hacs/integration": - repository.versions.installed = VERSION - repository.status.installed = True +"""Data handler for HACS.""" +import os +import asyncio + +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_default_encoder, + async_save_to_store, + get_store_for_key, +) +from custom_components.hacs.share import get_hacs + +from homeassistant.core import callback + + +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: + """HacsData class.""" + + def __init__(self): + """Initialize.""" + self.logger = getLogger() + self.hacs = get_hacs() + self.content = {} + + async def async_write(self): + """Write content to the store files.""" + if self.hacs.status.background_task or self.hacs.system.disabled: + return + + self.logger.debug("Saving data") + + # Hacs + await async_save_to_store( + self.hacs.hass, + "hacs", + { + "view": self.hacs.configuration.frontend_mode, + "compact": self.hacs.configuration.frontend_compact, + "onboarding_done": self.hacs.configuration.onboarding_done, + }, + ) + 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 = {} + # 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) + + async def async_store_repository_data(self, repository): + repository_manifest = repository.repository_manifest.manifest + data = { + "authors": repository.data.authors, + "category": repository.data.category, + "description": repository.data.description, + "domain": repository.data.domain, + "downloads": repository.data.downloads, + "etag_repository": repository.data.etag_repository, + "full_name": repository.data.full_name, + "first_install": repository.status.first_install, + "installed_commit": repository.data.installed_commit, + "installed": repository.data.installed, + "last_commit": repository.data.last_commit, + "last_release_tag": repository.data.last_version, + "last_updated": repository.data.last_updated, + "name": repository.data.name, + "new": repository.data.new, + "repository_manifest": repository_manifest, + "selected_tag": repository.data.selected_tag, + "show_beta": repository.data.show_beta, + "stars": repository.data.stargazers_count, + "topics": repository.data.topics, + "version_installed": repository.data.installed_version, + } + 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") or {} + + 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 + hass = self.hacs.hass + stores = {} + + try: + await self.register_unknown_repositories(repositories) + + 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") + + 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!", exc_info=exception) + return False + return True + + 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 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 + 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.data.last_updated = repository_data.get("last_updated") + repository.data.etag_repository = repository_data.get("etag_repository") + repository.data.topics = repository_data.get("topics", []) + repository.data.domain = repository_data.get("domain", None) + repository.data.stargazers_count = repository_data.get("stars", 0) + repository.releases.last_release = repository_data.get("last_release_tag") + repository.data.hide = repository_data.get("hide", False) + repository.data.installed = repository_data.get("installed", False) + repository.data.new = repository_data.get("new", True) + repository.data.selected_tag = repository_data.get("selected_tag") + repository.data.show_beta = repository_data.get("show_beta", False) + repository.data.last_version = repository_data.get("last_release_tag") + repository.data.last_commit = repository_data.get("last_commit") + repository.data.installed_version = repository_data.get("version_installed") + repository.data.installed_commit = repository_data.get("installed_commit") + + repository.repository_manifest = HacsManifest.from_dict( + repository_data.get("repository_manifest", {}) + ) + + if repository.data.installed: + repository.status.first_install = False + + if repository_data["full_name"] == "hacs/integration": + repository.data.installed_version = INTEGRATION_VERSION + repository.data.installed = True + + return True diff --git a/custom_components/hacs/hacsbase/exceptions.py b/custom_components/hacs/hacsbase/exceptions.py deleted file mode 100644 index 62e5389..0000000 --- a/custom_components/hacs/hacsbase/exceptions.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Custom Exceptions.""" - - -class HacsException(Exception): - """Super basic.""" - - -class HacsExpectedException(HacsException): - """For stuff that are expected.""" diff --git a/custom_components/hacs/hacsbase/hacs.py b/custom_components/hacs/hacsbase/hacs.py new file mode 100644 index 0000000..f3ab026 --- /dev/null +++ b/custom_components/hacs/hacsbase/hacs.py @@ -0,0 +1,406 @@ +"""Initialize the HACS base.""" +import json +from datetime import timedelta + +from aiogithubapi import AIOGitHubAPIException +from queueman import QueueManager +from queueman.exceptions import QueueManagerExecutionStillInProgress + +from custom_components.hacs.helpers import HacsHelpers +from custom_components.hacs.helpers.functions.get_list_from_default import ( + async_get_list_from_default, +) +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, +) + +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 = "yaml" + + +class Hacs(HacsBase, HacsHelpers): + """The base class of HACS, nested throughout the project.""" + + _repositories = [] + _repositories_by_id = {} + _repositories_by_full_name = {} + 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.""" + return self._repositories_by_id.get(str(repository_id)) + + def get_by_name(self, repository_full_name): + """Get repository by full_name.""" + 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 self._repositories_by_id + + @property + def sorted_by_name(self): + """Return a sorted(by name) list of repository objects.""" + return sorted(self.repositories, key=lambda x: x.display_name) + + @property + def sorted_by_repository_name(self): + """Return a sorted(by repository_name) list of repository objects.""" + return sorted(self.repositories, key=lambda x: x.data.full_name) + + async def register_repository(self, full_name, category, check=True): + """Register a repository.""" + await register_repository(full_name, category, check=check) + + 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() + await self.async_load_default_repositories() + await self.clear_out_removed_repositories() + + self.recuring_tasks.append( + self.hass.helpers.event.async_track_time_interval( + self.recurring_tasks_installed, timedelta(hours=2) + ) + ) + + self.recuring_tasks.append( + self.hass.helpers.event.async_track_time_interval( + self.recurring_tasks_all, timedelta(hours=25) + ) + ) + self.recuring_tasks.append( + self.hass.helpers.event.async_track_time_interval( + self.prosess_queue, timedelta(minutes=10) + ) + ) + + self.hass.bus.async_fire("hacs/reload", {"force": True}) + await self.recurring_tasks_installed() + + await self.prosess_queue() + + self.status.startup = False + self.status.background_task = False + self.hass.bus.async_fire("hacs/status", {}) + await self.async_set_stage(HacsStage.RUNNING) + + async def handle_critical_repositories_startup(self): + """Handled critical repositories during startup.""" + alert = False + critical = await async_load_from_store(self.hass, "critical") + if not critical: + return + for repo in critical: + if not repo["acknowledged"]: + alert = True + if alert: + self.log.critical("URGENT!: Check the HACS panel!") + self.hass.components.persistent_notification.create( + title="URGENT!", message="**Check the HACS panel!**" + ) + + async def handle_critical_repositories(self): + """Handled critical repositories during runtime.""" + # Get critical repositories + critical_queue = QueueManager() + instored = [] + critical = [] + was_installed = False + + try: + critical = await self.data_repo.get_contents("critical") + critical = json.loads(critical.content) + except AIOGitHubAPIException: + pass + + if not critical: + self.log.debug("No critical repositories") + return + + stored_critical = await async_load_from_store(self.hass, "critical") + + for stored in stored_critical or []: + instored.append(stored["repository"]) + + stored_critical = [] + + for repository in critical: + removed_repo = get_removed(repository["repository"]) + removed_repo.removal_type = "critical" + repo = self.get_by_name(repository["repository"]) + + stored = { + "repository": repository["repository"], + "reason": repository["reason"], + "link": repository["link"], + "acknowledged": True, + } + if repository["repository"] not in instored: + if repo is not None and repo.installed: + self.log.critical( + "Removing repository %s, it is marked as critical", + repository["repository"], + ) + was_installed = True + stored["acknowledged"] = False + # Remove from HACS + critical_queue.add(repository.uninstall()) + repo.remove() + + stored_critical.append(stored) + removed_repo.update_data(stored) + + # Uninstall + await critical_queue.execute() + + # Save to FS + await async_save_to_store(self.hass, "critical", stored_critical) + + # Restart HASS + if was_installed: + self.log.critical("Resarting Home Assistant") + self.hass.async_create_task(self.hass.async_stop(100)) + + async def prosess_queue(self, _notarealarg=None): + """Recurring tasks for installed repositories.""" + if not self.queue.has_pending_tasks: + self.log.debug("Nothing in the queue") + return + if self.queue.running: + self.log.debug("Queue is already running") + return + + can_update = await get_fetch_updates_for(self.github) + 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: + self.status.background_task = True + self.hass.bus.async_fire("hacs/status", {}) + try: + await self.queue.execute(can_update) + except QueueManagerExecutionStillInProgress: + pass + self.status.background_task = False + self.hass.bus.async_fire("hacs/status", {}) + + async def recurring_tasks_installed(self, _notarealarg=None): + """Recurring tasks for installed repositories.""" + self.log.debug("Starting recurring background task for installed repositories") + self.status.background_task = True + self.hass.bus.async_fire("hacs/status", {}) + + 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 + ): + self.queue.add(self.factory.safe_update(repository)) + + await self.handle_critical_repositories() + self.status.background_task = False + self.hass.bus.async_fire("hacs/status", {}) + await self.data.async_write() + self.log.debug("Recurring background task for installed repositories done") + + 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", {}) + + for repository in self.repositories: + if repository.data.category in self.common.categories: + self.queue.add(self.factory.safe_common_update(repository)) + + await self.async_load_default_repositories() + await self.clear_out_removed_repositories() + self.status.background_task = False + await self.data.async_write() + self.hass.bus.async_fire("hacs/status", {}) + self.hass.bus.async_fire("hacs/repository", {"action": "reload"}) + self.log.debug("Recurring background task for all repositories done") + + async def clear_out_removed_repositories(self): + """Clear out blaclisted repositories.""" + need_to_save = False + for removed in list_removed_repositories(): + repository = self.get_by_name(removed.repository) + if repository is not None: + if repository.data.installed and removed.removal_type != "critical": + self.log.warning( + f"You have {repository.data.full_name} installed with HACS " + + "this repository has been removed, please consider removing it. " + + f"Removal reason ({removed.removal_type})" + ) + else: + need_to_save = True + repository.remove() + + if need_to_save: + await self.data.async_write() + + async def async_load_default_repositories(self): + """Load known repositories.""" + self.log.info("Loading known repositories") + + for item in await async_get_list_from_default(HacsCategory.REMOVED): + removed = get_removed(item["repository"]) + removed.reason = item.get("reason") + removed.link = item.get("link") + removed.removal_type = item.get("removal_type") + + for category in self.common.categories or []: + self.queue.add(self.async_get_category_repositories(HacsCategory(category))) + + await self.prosess_queue() + + 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 is_removed(repo): + continue + repository = self.get_by_name(repo) + if repository is not None: + if str(repository.data.id) not in self.common.default: + self.common.default.append(str(repository.data.id)) + else: + 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/hacsbase/task_factory.py b/custom_components/hacs/hacsbase/task_factory.py deleted file mode 100644 index 8b32aa9..0000000 --- a/custom_components/hacs/hacsbase/task_factory.py +++ /dev/null @@ -1,75 +0,0 @@ -# pylint: disable=missing-docstring,invalid-name -import logging -import time -from datetime import timedelta -import asyncio -from aiogithubapi import AIOGitHubException - -from custom_components.hacs.hacsbase.exceptions import HacsException -from custom_components.hacs.helpers.register_repository import register_repository - - -max_concurrent_tasks = asyncio.Semaphore(15) -sleeper = 5 - -logger = logging.getLogger("hacs.factory") - - -class HacsTaskFactory: - def __init__(self): - self.tasks = [] - self.running = False - - async def execute(self): - if not self.tasks: - logger.debug("No tasks to execute") - return - if self.running: - logger.debug("Allready executing tasks") - return - try: - self.running = True - logger.info("Processing %s tasks", len(self.tasks)) - start = time.time() - await asyncio.gather(*self.tasks) - logger.info( - "Task processing of %s tasks completed in %s seconds", - len(self.tasks), - timedelta(seconds=round(time.time() - start)).seconds, - ) - self.tasks = [] - self.running = False - except RuntimeError: - logger.warning("RuntimeError, Clearing current tasks") - self.tasks = [] - self.running = False - - async def safe_common_update(self, repository): - async with max_concurrent_tasks: - try: - await repository.common_update() - except (AIOGitHubException, HacsException) as exception: - logger.error("%s - %s", repository.data.full_name, exception) - - # Due to GitHub ratelimits we need to sleep a bit - await asyncio.sleep(sleeper) - - async def safe_update(self, repository): - async with max_concurrent_tasks: - try: - await repository.update_repository() - except (AIOGitHubException, HacsException) as exception: - logger.error("%s - %s", repository.data.full_name, exception) - - # Due to GitHub ratelimits we need to sleep a bit - await asyncio.sleep(sleeper) - - async def safe_register(self, repo, category): - async with max_concurrent_tasks: - try: - await register_repository(repo, category) - except (AIOGitHubException, HacsException) as exception: - logger.error("%s - %s", repo, exception) - - # Due to GitHub ratelimits we need to sleep a bit - await asyncio.sleep(sleeper) diff --git a/custom_components/hacs/handler/__init__.py b/custom_components/hacs/handler/__init__.py deleted file mode 100644 index ab064c7..0000000 --- a/custom_components/hacs/handler/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Initialize handlers.""" diff --git a/custom_components/hacs/handler/__pycache__/__init__.cpython-37.pyc b/custom_components/hacs/handler/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index fad57cd..0000000 Binary files a/custom_components/hacs/handler/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/handler/__pycache__/download.cpython-37.pyc b/custom_components/hacs/handler/__pycache__/download.cpython-37.pyc deleted file mode 100644 index 967a54e..0000000 Binary files a/custom_components/hacs/handler/__pycache__/download.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/handler/__pycache__/template.cpython-37.pyc b/custom_components/hacs/handler/__pycache__/template.cpython-37.pyc deleted file mode 100644 index 45d7b07..0000000 Binary files a/custom_components/hacs/handler/__pycache__/template.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/handler/download.py b/custom_components/hacs/handler/download.py deleted file mode 100644 index 489c99d..0000000 --- a/custom_components/hacs/handler/download.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Download.""" -import os -import gzip -import shutil - -import aiofiles -import async_timeout -from integrationhelper import Logger -import backoff -from ..hacsbase.exceptions import HacsException - -from custom_components.hacs.globals import get_hacs - - -@backoff.on_exception(backoff.expo, Exception, max_tries=5) -async def async_download_file(url): - """ - Download files, and return the content. - """ - hacs = get_hacs() - logger = Logger("hacs.download.downloader") - if url is None: - return - - # There is a bug somewhere... TODO: Find that bug.... - if "tags/" in url: - url = url.replace("tags/", "") - - logger.debug(f"Downloading {url}") - - result = None - - with async_timeout.timeout(60, loop=hacs.hass.loop): - request = await hacs.session.get(url) - - # Make sure that we got a valid result - if request.status == 200: - result = await request.read() - else: - raise HacsException( - "Got status code {} when trying to download {}".format( - request.status, url - ) - ) - - return result - - -async def async_save_file(location, content): - """Save files.""" - logger = Logger("hacs.download.save") - logger.debug(f"Saving {location}") - mode = "w" - encoding = "utf-8" - errors = "ignore" - - if not isinstance(content, str): - mode = "wb" - encoding = None - errors = None - - try: - async with aiofiles.open( - location, mode=mode, encoding=encoding, errors=errors - ) as outfile: - await outfile.write(content) - outfile.close() - - # Create gz for .js files - if os.path.isfile(location): - if location.endswith(".js") or location.endswith(".css"): - with open(location, "rb") as f_in: - with gzip.open(location + ".gz", "wb") as f_out: - shutil.copyfileobj(f_in, f_out) - - # Remove with 2.0 - if "themes" in location and location.endswith(".yaml"): - filename = location.split("/")[-1] - base = location.split("/themes/")[0] - combined = f"{base}/themes/{filename}" - if os.path.exists(combined): - logger.info(f"Removing old theme file {combined}") - os.remove(combined) - - except Exception as error: # pylint: disable=broad-except - msg = "Could not write data to {} - {}".format(location, error) - logger.error(msg) - return False - - return os.path.exists(location) diff --git a/custom_components/hacs/helpers/__init__.py b/custom_components/hacs/helpers/__init__.py new file mode 100644 index 0000000..8c5755c --- /dev/null +++ b/custom_components/hacs/helpers/__init__.py @@ -0,0 +1,17 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from custom_components.hacs.helpers.methods import ( + HacsHelperMethods, + RepositoryHelperMethods, +) +from custom_components.hacs.helpers.properties import RepositoryHelperProperties + + +class RepositoryHelpers( + RepositoryHelperMethods, + RepositoryHelperProperties, +): + """Helper class for repositories""" + + +class HacsHelpers(HacsHelperMethods): + """Helper class for HACS""" diff --git a/custom_components/hacs/helpers/__pycache__/download.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/download.cpython-37.pyc deleted file mode 100644 index 5edd767..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/download.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/filters.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/filters.cpython-37.pyc deleted file mode 100644 index 9968577..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/filters.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/get_defaults.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/get_defaults.cpython-37.pyc deleted file mode 100644 index 8bd4bf4..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/get_defaults.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/information.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/information.cpython-37.pyc deleted file mode 100644 index d1d9806..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/information.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/install.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/install.cpython-37.pyc deleted file mode 100644 index 66ae99d..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/install.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/misc.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/misc.cpython-37.pyc deleted file mode 100644 index e5e2fa6..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/misc.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/network.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/network.cpython-37.pyc deleted file mode 100644 index f56af79..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/network.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/register_repository.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/register_repository.cpython-37.pyc deleted file mode 100644 index dcd9dee..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/register_repository.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/helpers/__pycache__/validate_repository.cpython-37.pyc b/custom_components/hacs/helpers/__pycache__/validate_repository.cpython-37.pyc deleted file mode 100644 index 9ca3f9a..0000000 Binary files a/custom_components/hacs/helpers/__pycache__/validate_repository.cpython-37.pyc and /dev/null differ diff --git a/customisations/domains/.gitkeep b/custom_components/hacs/helpers/classes/__init__.py similarity index 100% rename from customisations/domains/.gitkeep rename to custom_components/hacs/helpers/classes/__init__.py diff --git a/custom_components/hacs/helpers/classes/exceptions.py b/custom_components/hacs/helpers/classes/exceptions.py new file mode 100644 index 0000000..4532ee8 --- /dev/null +++ b/custom_components/hacs/helpers/classes/exceptions.py @@ -0,0 +1,17 @@ +"""Custom Exceptions.""" + + +class HacsException(Exception): + """Super basic.""" + + +class HacsRepositoryArchivedException(HacsException): + """For repositories that are archived.""" + + +class HacsNotModifiedException(HacsException): + """For responses that are not modified.""" + + +class HacsExpectedException(HacsException): + """For stuff that are expected.""" diff --git a/custom_components/hacs/repositories/manifest.py b/custom_components/hacs/helpers/classes/manifest.py similarity index 89% rename from custom_components/hacs/repositories/manifest.py rename to custom_components/hacs/helpers/classes/manifest.py index 4213d33..f303010 100644 --- a/custom_components/hacs/repositories/manifest.py +++ b/custom_components/hacs/helpers/classes/manifest.py @@ -1,42 +1,43 @@ -""" -Manifest handling of a repository. - -https://hacs.xyz/docs/publish/start#hacsjson -""" -from typing import List -import attr - -from custom_components.hacs.hacsbase.exceptions import HacsException - - -@attr.s(auto_attribs=True) -class HacsManifest: - """HacsManifest class.""" - - name: str = None - content_in_root: bool = False - zip_release: bool = False - filename: str = None - manifest: dict = {} - hacs: str = None - hide_default_branch: bool = False - domains: List[str] = [] - country: List[str] = [] - homeassistant: str = None - persistent_directory: str = None - iot_class: str = None - render_readme: bool = False - - @staticmethod - def from_dict(manifest: dict): - """Set attributes from dicts.""" - if manifest is None: - raise HacsException("Missing manifest data") - - manifest_data = HacsManifest() - - manifest_data.manifest = manifest - - for key in manifest: - setattr(manifest_data, key, manifest[key]) - return manifest_data +""" +Manifest handling of a repository. + +https://hacs.xyz/docs/publish/start#hacsjson +""" +from typing import List + +import attr + +from custom_components.hacs.helpers.classes.exceptions import HacsException + + +@attr.s(auto_attribs=True) +class HacsManifest: + """HacsManifest class.""" + + name: str = None + content_in_root: bool = False + zip_release: bool = False + filename: str = None + manifest: dict = {} + hacs: str = None + hide_default_branch: bool = False + domains: List[str] = [] + country: List[str] = [] + homeassistant: str = None + persistent_directory: str = None + iot_class: str = None + render_readme: bool = False + + @staticmethod + def from_dict(manifest: dict): + """Set attributes from dicts.""" + if manifest is None: + raise HacsException("Missing manifest data") + + manifest_data = HacsManifest() + + manifest_data.manifest = manifest + + for key in manifest: + setattr(manifest_data, key, manifest[key]) + return manifest_data diff --git a/custom_components/hacs/repositories/removed.py b/custom_components/hacs/helpers/classes/removed.py similarity index 80% rename from custom_components/hacs/repositories/removed.py rename to custom_components/hacs/helpers/classes/removed.py index 69955eb..fb5b9b9 100644 --- a/custom_components/hacs/repositories/removed.py +++ b/custom_components/hacs/helpers/classes/removed.py @@ -15,3 +15,7 @@ def update_data(self, data: dict): for key in data: if key in self.__dict__: setattr(self, key, data[key]) + + def to_json(self): + """Return a JSON representation of the data.""" + return attr.asdict(self) diff --git a/custom_components/hacs/helpers/classes/repository.py b/custom_components/hacs/helpers/classes/repository.py new file mode 100644 index 0000000..49ace24 --- /dev/null +++ b/custom_components/hacs/helpers/classes/repository.py @@ -0,0 +1,467 @@ +"""Repository.""" +# pylint: disable=broad-except, no-member +import json +import os +import tempfile +import zipfile +import shutil + +from aiogithubapi import AIOGitHubAPIException +from queueman import QueueManager + +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 +from custom_components.hacs.helpers.functions.download import async_download_file +from custom_components.hacs.helpers.functions.information import ( + get_info_md_content, + 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 +from custom_components.hacs.helpers.functions.validate_repository import ( + common_update_data, + common_validate, +) +from custom_components.hacs.helpers.functions.version_to_install import ( + version_to_install, +) +from custom_components.hacs.share import get_hacs + + +class RepositoryVersions: + """Versions.""" + + available = None + available_commit = None + installed = None + installed_commit = None + + +class RepositoryStatus: + """Repository status.""" + + hide = False + installed = False + last_updated = None + new = True + selected_tag = None + show_beta = False + track = True + updated_info = False + first_install = True + + +class RepositoryInformation: + """RepositoryInformation.""" + + additional_info = None + authors = [] + category = None + default_branch = None + description = "" + state = None + full_name = None + full_name_lower = None + file_name = None + javascript_type = None + homeassistant_version = None + last_updated = None + uid = None + stars = 0 + info = None + name = None + topics = [] + + +class RepositoryReleases: + """RepositoyReleases.""" + + last_release = None + last_release_object = None + last_release_object_downloads = None + published_tags = [] + objects = [] + releases = False + downloads = None + + +class RepositoryPath: + """RepositoryPath.""" + + local = None + remote = None + + +class RepositoryContent: + """RepositoryContent.""" + + path = None + files = [] + objects = [] + single = False + + +class HacsRepository(RepositoryHelpers): + """HacsRepository.""" + + def __init__(self): + """Set up HacsRepository.""" + self.hacs = get_hacs() + self.data = RepositoryData() + self.content = RepositoryContent() + self.content.path = RepositoryPath() + self.information = RepositoryInformation() + self.repository_object = None + self.status = RepositoryStatus() + self.state = None + self.force_branch = False + self.integration_manifest = {} + self.repository_manifest = HacsManifest.from_dict({}) + self.validate = Validate() + self.releases = RepositoryReleases() + self.versions = RepositoryVersions() + self.pending_restart = False + self.tree = [] + self.treefiles = [] + self.ref = None + self.logger = getLogger() + + def __str__(self) -> str: + """Return a string representation of the repository.""" + return f"<{self.data.category.title()} {self.data.full_name}>" + + @property + def display_name(self): + """Return display name.""" + return get_repository_name(self) + + @property + def display_status(self): + """Return display_status.""" + if self.data.new: + status = "new" + elif self.pending_restart: + status = "pending-restart" + elif self.pending_upgrade: + status = "pending-upgrade" + elif self.data.installed: + status = "installed" + else: + status = "default" + return status + + @property + def display_status_description(self): + """Return display_status_description.""" + description = { + "default": "Not installed.", + "pending-restart": "Restart pending.", + "pending-upgrade": "Upgrade pending.", + "installed": "No action required.", + "new": "This is a newly added repository.", + } + return description[self.display_status] + + @property + def display_installed_version(self): + """Return display_authors""" + if self.data.installed_version is not None: + installed = self.data.installed_version + else: + if self.data.installed_commit is not None: + installed = self.data.installed_commit + else: + installed = "" + return installed + + @property + def display_available_version(self): + """Return display_authors""" + if self.data.last_version is not None: + available = self.data.last_version + else: + if self.data.last_commit is not None: + available = self.data.last_commit + else: + available = "" + return available + + @property + def display_version_or_commit(self): + """Does the repositoriy use releases or commits?""" + if self.data.releases: + version_or_commit = "version" + else: + version_or_commit = "commit" + return version_or_commit + + @property + def main_action(self): + """Return the main action.""" + actions = { + "new": "INSTALL", + "default": "INSTALL", + "installed": "REINSTALL", + "pending-restart": "REINSTALL", + "pending-upgrade": "UPGRADE", + } + return actions[self.display_status] + + async def common_validate(self, ignore_issues=False): + """Common validation steps of the repository.""" + await common_validate(self, ignore_issues) + + async def common_registration(self): + """Common registration steps of the repository.""" + # Attach repository + if self.repository_object is None: + try: + self.repository_object, etag = await get_repository( + self.hacs.session, + self.hacs.configuration.token, + self.data.full_name, + None if self.data.installed else self.data.etag_repository, + ) + self.data.update_data(self.repository_object.attributes) + self.data.etag_repository = etag + except HacsNotModifiedException: + self.logger.debug( + "Did not update %s, content was not modified", self.data.full_name + ) + return + + # Set topics + self.data.topics = self.data.topics + + # Set stargazers_count + self.data.stargazers_count = self.data.stargazers_count + + # Set description + self.data.description = self.data.description + + if self.hacs.system.action: + if self.data.description is None or len(self.data.description) == 0: + raise HacsException("::error:: Missing repository description") + + async def common_update(self, ignore_issues=False, force=False): + """Common information update steps of the repository.""" + self.logger.debug("%s Getting repository information", self) + + # 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 + ) + return False + + # Update last updated + self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0) + + # Update last available commit + await self.repository_object.set_last_commit() + self.data.last_commit = self.repository_object.last_commit + + # Get the content of hacs.json + await self.get_repository_manifest_content() + + # Update "info.md" + self.information.additional_info = await get_info_md_content(self) + + return True + + async def download_zip_files(self, validate): + """Download ZIP archive from repository release.""" + download_queue = QueueManager() + try: + contents = False + + for release in self.releases.objects: + 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 + + if not contents: + return validate + + for content in contents or []: + download_queue.add(self.async_download_zip_file(content, validate)) + + await download_queue.execute() + except (Exception, BaseException): + validate.errors.append("Download was not completed") + + return validate + + async def async_download_zip_file(self, content, validate): + """Download ZIP archive from repository release.""" + try: + filecontent = await async_download_file(content.download_url) + + if filecontent is None: + validate.errors.append(f"[{content.name}] was not downloaded") + return + + temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp) + temp_file = f"{temp_dir}/{self.data.filename}" + + result = await async_save_file(temp_file, filecontent) + with zipfile.ZipFile(temp_file, "r") as zip_file: + zip_file.extractall(self.content.path.local) + + def cleanup_temp_dir(): + """Cleanup temp_dir.""" + if os.path.exists(temp_dir): + self.logger.debug("Cleaning up %s", temp_dir) + shutil.rmtree(temp_dir) + + if result: + self.logger.info("%s Download of %s completed", self, content.name) + await self.hacs.hass.async_add_executor_job(cleanup_temp_dir) + return + + validate.errors.append(f"[{content.name}] was not downloaded") + except (Exception, BaseException): + validate.errors.append("Download was not completed") + + return validate + + async def download_content(self, validate, _directory_path, _local_directory, _ref): + """Download the content of a directory.""" + from custom_components.hacs.helpers.functions.download import download_content + + validate = await download_content(self) + return validate + + async def get_repository_manifest_content(self): + """Get the content of the hacs.json file.""" + if not "hacs.json" in [x.filename for x in self.tree]: + if self.hacs.system.action: + 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) + + self.ref = version_to_install(self) + + try: + manifest = await self.repository_object.get_contents("hacs.json", self.ref) + 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: + raise HacsException( + f"::error:: hacs.json file is not valid ({exception})." + ) from None + if self.hacs.system.action: + self.logger.info("%s hacs.json is valid", self) + + def remove(self): + """Run remove tasks.""" + self.logger.info("%s Starting removal", self) + + if self.data.id in self.hacs.common.installed: + self.hacs.common.installed.remove(self.data.id) + for repository in self.hacs.repositories: + if repository.data.id == self.data.id: + self.hacs.async_remove_repository(repository) + + async def uninstall(self): + """Run uninstall tasks.""" + self.logger.info("%s Uninstalling", self) + if not await self.remove_local_directory(): + raise HacsException("Could not uninstall") + self.data.installed = False + if self.data.category == "integration": + if self.data.config_flow: + await self.reload_custom_components() + else: + self.pending_restart = True + elif self.data.category == "theme": + try: + 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: + self.hacs.common.installed.remove(self.data.full_name) + + await async_remove_store(self.hacs.hass, f"hacs/{self.data.id}.hacs") + + self.data.installed_version = None + self.data.installed_commit = None + self.hacs.hass.bus.async_fire( + "hacs/repository", + {"id": 1337, "action": "uninstall", "repository": self.data.full_name}, + ) + + async def remove_local_directory(self): + """Check the local directory.""" + from asyncio import sleep + + try: + if self.data.category == "python_script": + local_path = f"{self.content.path.local}/{self.data.name}.py" + elif self.data.category == "theme": + if os.path.exists( + f"{self.hacs.core.config_path}/{self.hacs.configuration.theme_path}/{self.data.name}.yaml" + ): + os.remove( + f"{self.hacs.core.config_path}/{self.hacs.configuration.theme_path}/{self.data.name}.yaml" + ) + local_path = self.content.path.local + elif self.data.category == "integration": + if not self.data.domain: + self.logger.error("%s Missing domain", self) + return False + local_path = self.content.path.local + else: + local_path = self.content.path.local + + 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 + ) + return False + self.logger.debug("%s Removing %s", self, local_path) + + if self.data.category in ["python_script"]: + os.remove(local_path) + else: + shutil.rmtree(local_path) + + while os.path.exists(local_path): + await sleep(1) + else: + self.logger.debug( + "%s Presumed local content path %s does not exist", self, local_path + ) + + except (Exception, BaseException) as 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 new file mode 100644 index 0000000..db127de --- /dev/null +++ b/custom_components/hacs/helpers/classes/repositorydata.py @@ -0,0 +1,149 @@ +"""Repository data.""" +from datetime import datetime +from typing import List, Optional + +import attr +import json + +from homeassistant.helpers.json import JSONEncoder + + +@attr.s(auto_attribs=True) +class RepositoryData: + """RepositoryData class.""" + + archived: bool = False + authors: List[str] = [] + category: str = "" + content_in_root: bool = False + country: List[str] = [] + config_flow: bool = False + default_branch: str = None + description: str = "" + domain: str = "" + domains: List[str] = [] + downloads: int = 0 + etag_repository: str = None + file_name: str = "" + filename: str = "" + first_install: bool = False + fork: bool = False + full_name: str = "" + hacs: str = None # Minimum HACS version + hide: bool = False + hide_default_branch: bool = False + homeassistant: str = None # Minimum Home Assistant version + id: int = 0 + iot_class: str = None + installed: bool = False + installed_commit: str = None + installed_version: str = None + open_issues: int = 0 + last_commit: str = None + last_version: str = None + last_updated: str = 0 + manifest_name: str = None + new: bool = True + persistent_directory: str = None + pushed_at: str = "" + releases: bool = False + render_readme: bool = False + published_tags: List[str] = [] + selected_tag: str = None + show_beta: bool = False + stargazers_count: int = 0 + topics: List[str] = [] + zip_release: bool = False + _storage_data: Optional[dict] = None + + @property + def stars(self): + """Return the stargazers count.""" + return self.stargazers_count or 0 + + @property + def name(self): + """Return the name.""" + if self.category in ["integration", "netdaemon"]: + return self.domain + return self.full_name.split("/")[-1] + + def to_json(self): + """Export to json.""" + return 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: + 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 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/classes/validate.py b/custom_components/hacs/helpers/classes/validate.py new file mode 100644 index 0000000..716da9c --- /dev/null +++ b/custom_components/hacs/helpers/classes/validate.py @@ -0,0 +1,11 @@ +class Validate: + """Validate.""" + + errors = [] + + @property + def success(self): + """Return bool if the validation was a success.""" + if self.errors: + return False + return True diff --git a/customisations/entities/.gitkeep b/custom_components/hacs/helpers/functions/__init__.py similarity index 100% rename from customisations/entities/.gitkeep rename to custom_components/hacs/helpers/functions/__init__.py diff --git a/custom_components/hacs/configuration_schema.py b/custom_components/hacs/helpers/functions/configuration_schema.py similarity index 53% rename from custom_components/hacs/configuration_schema.py rename to custom_components/hacs/helpers/functions/configuration_schema.py index 704ea4f..dfe632b 100644 --- a/custom_components/hacs/configuration_schema.py +++ b/custom_components/hacs/helpers/functions/configuration_schema.py @@ -1,16 +1,17 @@ """HACS Configuration Schemas.""" # pylint: disable=dangerous-default-value import voluptuous as vol -from .const import LOCALE + +from custom_components.hacs.const import LOCALE # Configuration: TOKEN = "token" SIDEPANEL_TITLE = "sidepanel_title" SIDEPANEL_ICON = "sidepanel_icon" +FRONTEND_REPO = "frontend_repo" +FRONTEND_REPO_URL = "frontend_repo_url" APPDAEMON = "appdaemon" NETDAEMON = "netdaemon" -PYTHON_SCRIPT = "python_script" -THEME = "theme" # Options: COUNTRY = "country" @@ -18,47 +19,47 @@ RELEASE_LIMIT = "release_limit" EXPERIMENTAL = "experimental" +# Config group +PATH_OR_URL = "frontend_repo_path_or_url" + -def hacs_base_config_schema(config: dict = {}, config_flow: bool = False) -> dict: +def hacs_base_config_schema(config: dict = {}) -> dict: """Return a shcema configuration dict for HACS.""" if not config: config = { TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxx", - SIDEPANEL_ICON: "mdi:alpha-c-box", - SIDEPANEL_TITLE: "HACS", - APPDAEMON: False, - NETDAEMON: False, - PYTHON_SCRIPT: False, - THEME: False, - } - if config_flow: - return { - vol.Required(TOKEN, default=config.get(TOKEN)): str, - vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str, - vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str, - vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool, - vol.Optional(NETDAEMON, default=config.get(NETDAEMON)): bool, } return { vol.Required(TOKEN, default=config.get(TOKEN)): str, - vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str, - vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str, - vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool, - vol.Optional(NETDAEMON, default=config.get(NETDAEMON)): bool, - vol.Optional(PYTHON_SCRIPT, default=config.get(PYTHON_SCRIPT)): bool, - vol.Optional(THEME, default=config.get(THEME)): bool, } def hacs_config_option_schema(options: dict = {}) -> dict: """Return a shcema for HACS configuration options.""" if not options: - options = {COUNTRY: "ALL", DEBUG: False, RELEASE_LIMIT: 5, EXPERIMENTAL: False} + options = { + APPDAEMON: False, + COUNTRY: "ALL", + DEBUG: False, + EXPERIMENTAL: False, + NETDAEMON: False, + RELEASE_LIMIT: 5, + SIDEPANEL_ICON: "hacs:hacs", + SIDEPANEL_TITLE: "HACS", + FRONTEND_REPO: "", + FRONTEND_REPO_URL: "", + } return { - vol.Optional(COUNTRY, default=options.get(COUNTRY)): vol.In(LOCALE), + vol.Optional(SIDEPANEL_TITLE, default=options.get(SIDEPANEL_TITLE)): str, + vol.Optional(SIDEPANEL_ICON, default=options.get(SIDEPANEL_ICON)): str, vol.Optional(RELEASE_LIMIT, default=options.get(RELEASE_LIMIT)): int, - vol.Optional(EXPERIMENTAL, default=options.get(EXPERIMENTAL)): bool, + vol.Optional(COUNTRY, default=options.get(COUNTRY)): vol.In(LOCALE), + vol.Optional(APPDAEMON, default=options.get(APPDAEMON)): bool, + vol.Optional(NETDAEMON, default=options.get(NETDAEMON)): bool, vol.Optional(DEBUG, default=options.get(DEBUG)): bool, + vol.Optional(EXPERIMENTAL, default=options.get(EXPERIMENTAL)): bool, + vol.Exclusive(FRONTEND_REPO, PATH_OR_URL): str, + vol.Exclusive(FRONTEND_REPO_URL, PATH_OR_URL): str, } diff --git a/custom_components/hacs/helpers/functions/constrains.py b/custom_components/hacs/helpers/functions/constrains.py new file mode 100644 index 0000000..93b4e07 --- /dev/null +++ b/custom_components/hacs/helpers/functions/constrains.py @@ -0,0 +1,43 @@ +"""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/download.py b/custom_components/hacs/helpers/functions/download.py similarity index 61% rename from custom_components/hacs/helpers/download.py rename to custom_components/hacs/helpers/functions/download.py index 20414db..73f490e 100644 --- a/custom_components/hacs/helpers/download.py +++ b/custom_components/hacs/helpers/functions/download.py @@ -1,10 +1,22 @@ """Helpers to download repository content.""" +import os import pathlib import tempfile import zipfile -from custom_components.hacs.hacsbase.exceptions import HacsException -from custom_components.hacs.handler.download import async_download_file, async_save_file -from custom_components.hacs.helpers.filters import filter_content_return_one_of_type + +import async_timeout +import backoff +from queueman import QueueManager, concurrent + +from custom_components.hacs.helpers.classes.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 + +_LOGGER = getLogger() class FileInformation: @@ -14,6 +26,36 @@ def __init__(self, url, path, name): self.name = name +@backoff.on_exception(backoff.expo, Exception, max_tries=5) +async def async_download_file(url): + """Download files, and return the content.""" + hacs = get_hacs() + if url is None: + return + + if "tags/" in url: + url = url.replace("tags/", "") + + _LOGGER.debug("Downloading %s", url) + + result = None + + with async_timeout.timeout(60, loop=hacs.hass.loop): + request = await hacs.session.get(url) + + # Make sure that we got a valid result + if request.status == 200: + result = await request.read() + else: + raise HacsException( + "Got status code {} when trying to download {}".format( + request.status, url + ) + ) + + return result + + def should_try_releases(repository): """Return a boolean indicating whether to download releases or not.""" if repository.data.zip_release: @@ -24,7 +66,7 @@ def should_try_releases(repository): return False if repository.data.category not in ["plugin", "theme"]: return False - if not repository.releases.releases: + if not repository.data.releases: return False return True @@ -94,9 +136,10 @@ def gather_files_to_download(repository): return files -async def download_zip(repository, validate): +async def download_zip_files(repository, validate): """Download ZIP archive from repository release.""" contents = [] + queue = QueueManager() try: for release in repository.releases.objects: repository.logger.info( @@ -108,33 +151,48 @@ async def download_zip(repository, validate): if not contents: return validate - for content in contents: - filecontent = await async_download_file(content.download_url) + for content in contents or []: + queue.add(async_download_zip_file(repository, content, validate)) - if filecontent is None: - validate.errors.append(f"[{content.name}] was not downloaded.") - continue + await queue.execute() + except (Exception, BaseException) as exception: # pylint: disable=broad-except + validate.errors.append(f"Download was not completed [{exception}]") - result = await async_save_file( - f"{tempfile.gettempdir()}/{repository.data.filename}", filecontent - ) - with zipfile.ZipFile( - f"{tempfile.gettempdir()}/{repository.data.filename}", "r" - ) as zip_file: - zip_file.extractall(repository.content.path.local) + return validate - if result: - repository.logger.info(f"download of {content.name} complete") - continue + +async def async_download_zip_file(repository, content, validate): + """Download ZIP archive from repository release.""" + try: + filecontent = await async_download_file(content.download_url) + + if filecontent is None: validate.errors.append(f"[{content.name}] was not downloaded.") - except Exception as exception: # pylint: disable=broad-except - validate.errors.append(f"Download was not complete [{exception}]") + return + + result = await async_save_file( + f"{tempfile.gettempdir()}/{repository.data.filename}", filecontent + ) + with zipfile.ZipFile( + f"{tempfile.gettempdir()}/{repository.data.filename}", "r" + ) as zip_file: + zip_file.extractall(repository.content.path.local) + + os.remove(f"{tempfile.gettempdir()}/{repository.data.filename}") + + if result: + repository.logger.info(f"Download of {content.name} completed") + return + validate.errors.append(f"[{content.name}] was not downloaded.") + except (Exception, BaseException) as exception: # pylint: disable=broad-except + validate.errors.append(f"Download was not completed [{exception}]") return validate async def download_content(repository): """Download the content of a directory.""" + queue = QueueManager() contents = gather_files_to_download(repository) repository.logger.debug(repository.data.filename) if not contents: @@ -144,13 +202,22 @@ async def download_content(repository): if repository.data.content_in_root and repository.data.filename: if content.name != repository.data.filename: continue + queue.add(dowload_repository_content(repository, content)) + await queue.execute() + return repository.validate + + +@concurrent(10) +async def dowload_repository_content(repository, content): + """Download content.""" + try: repository.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file(content.download_url) if filecontent is None: repository.validate.errors.append(f"[{content.name}] was not downloaded.") - continue + return # Save the content of the file. if repository.content.single or content.path is None: @@ -175,7 +242,9 @@ async def download_content(repository): result = await async_save_file(local_file_path, filecontent) if result: - repository.logger.info(f"download of {content.name} complete") - continue + 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/filters.py b/custom_components/hacs/helpers/functions/filters.py similarity index 100% rename from custom_components/hacs/helpers/filters.py rename to custom_components/hacs/helpers/functions/filters.py diff --git a/custom_components/hacs/helpers/functions/get_list_from_default.py b/custom_components/hacs/helpers/functions/get_list_from_default.py new file mode 100644 index 0000000..3b4e5ba --- /dev/null +++ b/custom_components/hacs/helpers/functions/get_list_from_default.py @@ -0,0 +1,31 @@ +"""Helper to get default repositories.""" +import json +from typing import List + +from aiogithubapi import AIOGitHubAPIException + +from custom_components.hacs.enums import HacsCategory +from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.share import get_hacs + + +async def async_get_list_from_default(default: HacsCategory) -> List: + """Get repositories from default list.""" + hacs = get_hacs() + repositories = [] + + try: + content = await hacs.data_repo.get_contents( + default, hacs.data_repo.default_branch + ) + repositories = json.loads(content.content) + + except (AIOGitHubAPIException, HacsException) as exception: + hacs.log.error(exception) + + except (Exception, BaseException) as exception: + hacs.log.error(exception) + + hacs.log.debug("Got %s elements for %s", len(repositories), default) + + return repositories diff --git a/custom_components/hacs/helpers/information.py b/custom_components/hacs/helpers/functions/information.py similarity index 65% rename from custom_components/hacs/helpers/information.py rename to custom_components/hacs/helpers/functions/information.py index 2fde31f..4e9b841 100644 --- a/custom_components/hacs/helpers/information.py +++ b/custom_components/hacs/helpers/functions/information.py @@ -1,8 +1,15 @@ """Return repository information if any.""" import json -from aiogithubapi import AIOGitHubException, AIOGitHub -from custom_components.hacs.handler.template import render_template -from custom_components.hacs.hacsbase.exceptions import HacsException + +from aiogithubapi import AIOGitHubAPIException, GitHub, AIOGitHubAPINotModifiedException + +from custom_components.hacs.helpers.classes.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): @@ -29,18 +36,30 @@ async def get_info_md_content(repository): return "" info = info.content.replace(" 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 diff --git a/custom_components/hacs/helpers/functions/logger.py b/custom_components/hacs/helpers/functions/logger.py new file mode 100644 index 0000000..900bc91 --- /dev/null +++ b/custom_components/hacs/helpers/functions/logger.py @@ -0,0 +1,19 @@ +"""Custom logger for HACS.""" +# pylint: disable=invalid-name +import logging +import os + +from ...const import PACKAGE_NAME + +_HACSLogger: logging.Logger = logging.getLogger(PACKAGE_NAME) + +if "GITHUB_ACTION" in os.environ: + logging.basicConfig( + format="::%(levelname)s:: %(message)s", + level="DEBUG", + ) + + +def getLogger(_name: str = None) -> logging.Logger: + """Return a Logger instance.""" + return _HACSLogger diff --git a/custom_components/hacs/helpers/misc.py b/custom_components/hacs/helpers/functions/misc.py similarity index 54% rename from custom_components/hacs/helpers/misc.py rename to custom_components/hacs/helpers/functions/misc.py index aeb19ed..5114d6b 100644 --- a/custom_components/hacs/helpers/misc.py +++ b/custom_components/hacs/helpers/functions/misc.py @@ -1,5 +1,12 @@ """Helper functions: misc""" -import semantic_version +import re +from functools import lru_cache + +from awesomeversion import AwesomeVersion + +RE_REPOSITORY = re.compile( + r"(?:(?:.*github.com.)|^)([A-Za-z0-9-]+\/[\w.-]+?)(?:(?:\.git)?|(?:[^\w.-].*)?)$" +) def get_repository_name(repository) -> str: @@ -21,10 +28,15 @@ def get_repository_name(repository) -> str: ) -def version_left_higher_then_right(new: str, old: str) -> bool: +@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.""" - if not isinstance(new, str) or not isinstance(old, str): - return False - if new == old: - return True - return semantic_version.Version.coerce(new) > semantic_version.Version.coerce(old) + return AwesomeVersion(left) >= AwesomeVersion(right) + + +def extract_repository_from_url(url: str) -> str or None: + """Extract the owner/repo part form a URL.""" + match = re.match(RE_REPOSITORY, url) + if not match: + return None + return match.group(1).lower() diff --git a/custom_components/hacs/helpers/functions/path_exsist.py b/custom_components/hacs/helpers/functions/path_exsist.py new file mode 100644 index 0000000..5e941d6 --- /dev/null +++ b/custom_components/hacs/helpers/functions/path_exsist.py @@ -0,0 +1,13 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +import os + +from custom_components.hacs.share import get_hacs + + +def path_exsist(path) -> bool: + return os.path.exists(path) + + +async def async_path_exsist(path) -> bool: + hass = get_hacs().hass + return await hass.async_add_executor_job(path_exsist, path) diff --git a/custom_components/hacs/helpers/functions/register_repository.py b/custom_components/hacs/helpers/functions/register_repository.py new file mode 100644 index 0000000..62868ed --- /dev/null +++ b/custom_components/hacs/helpers/functions/register_repository.py @@ -0,0 +1,65 @@ +"""Register a repository.""" +from aiogithubapi import AIOGitHubAPIException + +from custom_components.hacs.helpers.classes.exceptions import ( + HacsException, + HacsExpectedException, +) +from custom_components.hacs.share import get_hacs + +from ...repositories import RERPOSITORY_CLASSES + + +# @concurrent(15, 5) +async def register_repository(full_name, category, check=True, ref=None): + """Register a repository.""" + hacs = get_hacs() + + if full_name in hacs.common.skip: + if full_name != "hacs/integration": + raise HacsExpectedException(f"Skipping {full_name}") + + if category not in RERPOSITORY_CLASSES: + raise HacsException(f"{category} is not a valid repository category.") + + repository = RERPOSITORY_CLASSES[category](full_name) + if check: + try: + await repository.async_registration(ref) + if hacs.status.new: + repository.data.new = False + if repository.validate.errors: + hacs.common.skip.append(repository.data.full_name) + if not hacs.status.startup: + hacs.log.error("Validation for %s failed.", full_name) + if hacs.system.action: + raise HacsException(f"::error:: Validation for {full_name} failed.") + return repository.validate.errors + if hacs.system.action: + repository.logger.info("%s Validation completed", repository) + else: + repository.logger.info("%s Registration completed", repository) + except AIOGitHubAPIException as exception: + hacs.common.skip.append(repository.data.full_name) + raise HacsException( + f"Validation for {full_name} failed with {exception}." + ) from None + + 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 + ): + hacs.hass.bus.async_fire( + "hacs/repository", + { + "action": "registration", + "repository": repository.data.full_name, + "repository_id": repository.data.id, + }, + ) + 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 new file mode 100644 index 0000000..27a0afe --- /dev/null +++ b/custom_components/hacs/helpers/functions/remaining_github_calls.py @@ -0,0 +1,32 @@ +"""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 new file mode 100644 index 0000000..05a6e03 --- /dev/null +++ b/custom_components/hacs/helpers/functions/save.py @@ -0,0 +1,52 @@ +"""Download.""" +import gzip +import os +import shutil + +import aiofiles + +from custom_components.hacs.helpers.functions.logger import getLogger + +_LOGGER = getLogger() + + +async def async_save_file(location, content): + """Save files.""" + _LOGGER.debug("Saving %s", location) + mode = "w" + encoding = "utf-8" + errors = "ignore" + + if not isinstance(content, str): + mode = "wb" + encoding = None + errors = None + + try: + async with aiofiles.open( + location, mode=mode, encoding=encoding, errors=errors + ) as outfile: + await outfile.write(content) + outfile.close() + + # Create gz for .js files + if os.path.isfile(location): + if location.endswith(".js") or location.endswith(".css"): + with open(location, "rb") as f_in: + with gzip.open(location + ".gz", "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + # Remove with 2.0 + if "themes" in location and location.endswith(".yaml"): + filename = location.split("/")[-1] + base = location.split("/themes/")[0] + combined = f"{base}/themes/{filename}" + if os.path.exists(combined): + _LOGGER.info("Removing old theme file %s", combined) + os.remove(combined) + + except (Exception, BaseException) as error: # pylint: disable=broad-except + _LOGGER.error("Could not write data to %s - %s", location, error) + return False + + return os.path.exists(location) diff --git a/custom_components/hacs/helpers/functions/store.py b/custom_components/hacs/helpers/functions/store.py new file mode 100644 index 0000000..45d76ef --- /dev/null +++ b/custom_components/hacs/helpers/functions/store.py @@ -0,0 +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 + +_LOGGER = getLogger() + + +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.""" + return HACSStore(hass, VERSION_STORAGE, get_store_key(key), encoder=encoder) + + +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.""" + 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. + + 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", + get_store_key(key), + ) + + +async def async_remove_store(hass, key): + """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/handler/template.py b/custom_components/hacs/helpers/functions/template.py similarity index 72% rename from custom_components/hacs/handler/template.py rename to custom_components/hacs/helpers/functions/template.py index ca77e7a..4f59840 100644 --- a/custom_components/hacs/handler/template.py +++ b/custom_components/hacs/helpers/functions/template.py @@ -1,29 +1,32 @@ -"""Custom template support.""" -# pylint: disable=broad-except -from jinja2 import Template -from integrationhelper import Logger - - -def render_template(content, context): - """Render templates in content.""" - # Fix None issues - if context.releases.last_release_object is not None: - prerelease = context.releases.last_release_object.prerelease - else: - prerelease = False - - # Render the template - try: - render = Template(content) - render = render.render( - installed=context.status.installed, - pending_update=context.pending_upgrade, - prerelease=prerelease, - selected_tag=context.status.selected_tag, - version_available=context.releases.last_release, - version_installed=context.display_installed_version, - ) - return render - except Exception as exception: - Logger("hacs.template").debug(exception) - return content +"""Custom template support.""" +# pylint: disable=broad-except +from jinja2 import Template + +from custom_components.hacs.helpers.functions.logger import getLogger + +_LOGGER = getLogger() + + +def render_template(content, context): + """Render templates in content.""" + # Fix None issues + if context.releases.last_release_object is not None: + prerelease = context.releases.last_release_object.prerelease + else: + prerelease = False + + # Render the template + try: + render = Template(content) + render = render.render( + installed=context.data.installed, + pending_update=context.pending_upgrade, + prerelease=prerelease, + selected_tag=context.data.selected_tag, + version_available=context.releases.last_release, + version_installed=context.display_installed_version, + ) + return render + except (Exception, BaseException) as exception: + _LOGGER.debug(exception) + return content diff --git a/custom_components/hacs/helpers/functions/validate_repository.py b/custom_components/hacs/helpers/functions/validate_repository.py new file mode 100644 index 0000000..7a88d70 --- /dev/null +++ b/custom_components/hacs/helpers/functions/validate_repository.py @@ -0,0 +1,110 @@ +"""Helper to do common validation for repositories.""" +from aiogithubapi import AIOGitHubAPIException + +from custom_components.hacs.helpers.classes.exceptions import ( + HacsException, + HacsNotModifiedException, + HacsRepositoryArchivedException, +) +from custom_components.hacs.helpers.functions.information import ( + get_releases, + get_repository, + get_tree, +) +from custom_components.hacs.helpers.functions.version_to_install import ( + version_to_install, +) +from custom_components.hacs.share import get_hacs, is_removed + + +async def common_validate(repository, ignore_issues=False): + """Common validation steps of the repository.""" + repository.validate.errors = [] + + # Make sure the repository exist. + repository.logger.debug("%s Checking repository.", repository) + await common_update_data(repository, ignore_issues) + + # Step 6: Get the content of hacs.json + await repository.get_repository_manifest_content() + + +async def common_update_data(repository, ignore_issues=False, force=False): + """Common update data.""" + hacs = get_hacs() + releases = [] + try: + repository_object, etag = await get_repository( + hacs.session, + hacs.configuration.token, + repository.data.full_name, + etag=None + if force or repository.data.installed + else repository.data.etag_repository, + ) + repository.repository_object = repository_object + repository.data.update_data(repository_object.attributes) + repository.data.etag_repository = etag + except HacsNotModifiedException: + return + except (AIOGitHubAPIException, HacsException) as exception: + if not hacs.status.startup: + repository.logger.error("%s %s", repository, exception) + if not ignore_issues: + repository.validate.errors.append("Repository does not exist.") + raise HacsException(exception) from None + + # Make sure the repository is not archived. + if repository.data.archived and not ignore_issues: + repository.validate.errors.append("Repository is archived.") + raise HacsRepositoryArchivedException("Repository is archived.") + + # Make sure the repository is not in the blacklist. + if is_removed(repository.data.full_name) and not ignore_issues: + repository.validate.errors.append("Repository is in the blacklist.") + raise HacsException("Repository is in the blacklist.") + + # Get releases. + try: + releases = await get_releases( + repository.repository_object, + repository.data.show_beta, + hacs.configuration.release_limit, + ) + 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.last_version = next(iter(repository.data.published_tags)) + + except (AIOGitHubAPIException, HacsException): + repository.data.releases = False + + if not repository.force_branch: + repository.ref = version_to_install(repository) + if repository.data.releases: + for release in repository.releases.objects or []: + if release.tag_name == repository.ref: + assets = release.assets + if assets: + downloads = next(iter(assets)).attributes.get("download_count") + repository.data.downloads = downloads + + repository.logger.debug( + "%s Running checks against %s", repository, repository.ref.replace("tags/", "") + ) + + try: + repository.tree = await get_tree(repository.repository_object, repository.ref) + if not repository.tree: + raise HacsException("No files in tree") + repository.treefiles = [] + for treefile in repository.tree: + repository.treefiles.append(treefile.full_path) + except (AIOGitHubAPIException, HacsException) as exception: + if not hacs.status.startup: + repository.logger.error("%s %s", repository, exception) + if not ignore_issues: + raise HacsException(exception) from None diff --git a/custom_components/hacs/helpers/functions/version_to_install.py b/custom_components/hacs/helpers/functions/version_to_install.py new file mode 100644 index 0000000..75dd4ba --- /dev/null +++ b/custom_components/hacs/helpers/functions/version_to_install.py @@ -0,0 +1,20 @@ +"""Install helper for repositories.""" + + +def version_to_install(repository): + """Determine which version to isntall.""" + if repository.data.last_version is not None: + if repository.data.selected_tag is not None: + if repository.data.selected_tag == repository.data.last_version: + repository.data.selected_tag = None + return repository.data.last_version + return repository.data.selected_tag + return repository.data.last_version + if repository.data.selected_tag is not None: + if repository.data.selected_tag == repository.data.default_branch: + return repository.data.default_branch + if repository.data.selected_tag in repository.data.published_tags: + return repository.data.selected_tag + if repository.data.default_branch is None: + return "main" + return repository.data.default_branch diff --git a/custom_components/hacs/helpers/get_defaults.py b/custom_components/hacs/helpers/get_defaults.py deleted file mode 100644 index 744540d..0000000 --- a/custom_components/hacs/helpers/get_defaults.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Helpers to get default repositories.""" -import json -from aiogithubapi import AIOGitHub, AIOGitHubException -from integrationhelper import Logger -from custom_components.hacs.helpers.information import get_repository - - -async def get_default_repos_orgs(github: type(AIOGitHub), category: str) -> dict: - """Gets default org repositories.""" - repositories = [] - logger = Logger("hacs") - orgs = { - "plugin": "custom-cards", - "integration": "custom-components", - "theme": "home-assistant-community-themes", - } - if category not in orgs: - return repositories - - try: - repos = await github.get_org_repos(orgs[category]) - for repo in repos: - repositories.append(repo.full_name) - - except AIOGitHubException as exception: - logger.error(exception) - - return repositories - - -async def get_default_repos_lists(session, token, default: str) -> dict: - """Gets repositories from default list.""" - repositories = [] - logger = Logger("hacs") - - try: - repo = await get_repository(session, token, "hacs/default") - content = await repo.get_contents(default) - repositories = json.loads(content.content) - - except AIOGitHubException as exception: - logger.error(exception) - - return repositories diff --git a/custom_components/hacs/helpers/install.py b/custom_components/hacs/helpers/install.py deleted file mode 100644 index 7eab265..0000000 --- a/custom_components/hacs/helpers/install.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Install helper for repositories.""" -import os -import tempfile -from custom_components.hacs.globals import get_hacs -from custom_components.hacs.hacsbase.exceptions import HacsException -from custom_components.hacs.hacsbase.backup import Backup, BackupNetDaemon -from custom_components.hacs.helpers.download import download_content - - -async def install_repository(repository): - """Common installation steps of the repository.""" - persistent_directory = None - await repository.update_repository() - repository.validate.errors = [] - - if not repository.can_install: - raise HacsException( - "The version of Home Assistant is not compatible with this version" - ) - - version = version_to_install(repository) - if version == repository.data.default_branch: - repository.ref = version - else: - repository.ref = f"tags/{version}" - - if repository.status.installed and repository.data.category == "netdaemon": - persistent_directory = BackupNetDaemon(repository) - persistent_directory.create() - - elif repository.data.persistent_directory: - if os.path.exists( - f"{repository.content.path.local}/{repository.data.persistent_directory}" - ): - persistent_directory = Backup( - f"{repository.content.path.local}/{repository.data.persistent_directory}", - tempfile.gettempdir() + "/hacs_persistent_directory/", - ) - persistent_directory.create() - - if repository.status.installed and not repository.content.single: - backup = Backup(repository.content.path.local) - backup.create() - - if repository.data.zip_release and version != repository.data.default_branch: - await repository.download_zip(repository) - else: - await download_content(repository) - - if repository.validate.errors: - for error in repository.validate.errors: - repository.logger.error(error) - if repository.status.installed and not repository.content.single: - backup.restore() - - if repository.status.installed and not repository.content.single: - backup.cleanup() - - if persistent_directory is not None: - persistent_directory.restore() - persistent_directory.cleanup() - - if repository.validate.success: - if repository.data.full_name not in repository.hacs.common.installed: - if repository.data.full_name == "hacs/integration": - repository.hacs.common.installed.append(repository.data.full_name) - repository.status.installed = True - repository.versions.installed_commit = repository.versions.available_commit - - if version == repository.data.default_branch: - repository.versions.installed = None - else: - repository.versions.installed = version - - await reload_after_install(repository) - installation_complete(repository) - - -async def reload_after_install(repository): - """Reload action after installation success.""" - if repository.data.category == "integration": - if repository.config_flow: - if repository.data.full_name != "hacs/integration": - await repository.reload_custom_components() - repository.pending_restart = True - - elif repository.data.category == "theme": - try: - await repository.hacs.hass.services.async_call( - "frontend", "reload_themes", {} - ) - except Exception: # pylint: disable=broad-except - pass - elif repository.data.category == "netdaemon": - try: - await repository.hacs.hass.services.async_call( - "hassio", "addon_restart", {"addon": "e466aeb3_netdaemon"} - ) - except Exception: # pylint: disable=broad-except - pass - - -def installation_complete(repository): - """Action to run when the installation is complete.""" - hacs = get_hacs() - hacs.hass.bus.async_fire( - "hacs/repository", - {"id": 1337, "action": "install", "repository": repository.data.full_name}, - ) - - -def version_to_install(repository): - """Determine which version to isntall.""" - if repository.versions.available is not None: - if repository.status.selected_tag is not None: - if repository.status.selected_tag == repository.versions.available: - repository.status.selected_tag = None - return repository.versions.available - return repository.status.selected_tag - return repository.versions.available - if repository.status.selected_tag is not None: - if repository.status.selected_tag == repository.data.default_branch: - return repository.data.default_branch - if repository.status.selected_tag in repository.releases.published_tags: - return repository.status.selected_tag - if repository.data.default_branch is None: - return "master" - return repository.data.default_branch diff --git a/custom_components/hacs/helpers/methods/__init__.py b/custom_components/hacs/helpers/methods/__init__.py new file mode 100644 index 0000000..26d9422 --- /dev/null +++ b/custom_components/hacs/helpers/methods/__init__.py @@ -0,0 +1,30 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from custom_components.hacs.helpers.methods.installation import ( + RepositoryMethodInstall, + RepositoryMethodPostInstall, + RepositoryMethodPreInstall, +) +from custom_components.hacs.helpers.methods.registration import ( + RepositoryMethodPostRegistration, + RepositoryMethodPreRegistration, + RepositoryMethodRegistration, +) +from custom_components.hacs.helpers.methods.reinstall_if_needed import ( + RepositoryMethodReinstallIfNeeded, +) + + +class RepositoryHelperMethods( + RepositoryMethodReinstallIfNeeded, + RepositoryMethodInstall, + RepositoryMethodPostInstall, + RepositoryMethodPreInstall, + RepositoryMethodPreRegistration, + RepositoryMethodRegistration, + RepositoryMethodPostRegistration, +): + """Collection of repository methods that are nested to all repositories.""" + + +class HacsHelperMethods: + """Helper class for HACS methods""" diff --git a/custom_components/hacs/helpers/methods/installation.py b/custom_components/hacs/helpers/methods/installation.py new file mode 100644 index 0000000..c31dc12 --- /dev/null +++ b/custom_components/hacs/helpers/methods/installation.py @@ -0,0 +1,117 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +import os +import tempfile +from abc import ABC + +from custom_components.hacs.helpers.classes.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, +) +from custom_components.hacs.operational.backup import Backup, BackupNetDaemon +from custom_components.hacs.share import get_hacs + + +class RepositoryMethodPreInstall(ABC): + async def async_pre_install(self) -> None: + pass + + async def _async_pre_install(self) -> None: + self.logger.info("Running pre installation steps") + await self.async_pre_install() + self.logger.info("Pre installation steps completed") + + +class RepositoryMethodInstall(ABC): + async def async_install(self) -> None: + await self._async_pre_install() + self.logger.info("Running installation steps") + await async_install_repository(self) + self.logger.info("Installation steps completed") + await self._async_post_install() + + +class RepositoryMethodPostInstall(ABC): + async def async_post_installation(self) -> None: + pass + + async def _async_post_install(self) -> None: + self.logger.info("Running post installation steps") + await self.async_post_installation() + self.data.new = False + self.hacs.hass.bus.async_fire( + "hacs/repository", + {"id": 1337, "action": "install", "repository": self.data.full_name}, + ) + self.logger.info("Post installation steps completed") + + +async def async_install_repository(repository): + """Common installation steps of the repository.""" + hacs = get_hacs() + persistent_directory = None + await repository.update_repository() + if repository.content.path.local is None: + raise HacsException("repository.content.path.local is None") + repository.validate.errors = [] + + if not repository.can_install: + raise HacsException( + "The version of Home Assistant is not compatible with this version" + ) + + version = version_to_install(repository) + if version == repository.data.default_branch: + repository.ref = version + else: + 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 + ) + await hacs.hass.async_add_executor_job(persistent_directory.create) + + elif repository.data.persistent_directory: + if os.path.exists( + f"{repository.content.path.local}/{repository.data.persistent_directory}" + ): + persistent_directory = Backup( + f"{repository.content.path.local}/{repository.data.persistent_directory}", + tempfile.gettempdir() + "/hacs_persistent_directory/", + ) + await hacs.hass.async_add_executor_job(persistent_directory.create) + + if repository.data.installed and not repository.content.single: + backup = Backup(repository.content.path.local) + 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.validate) + else: + await download_content(repository) + + if repository.validate.errors: + for error in repository.validate.errors: + repository.logger.error(error) + if repository.data.installed and not repository.content.single: + await hacs.hass.async_add_executor_job(backup.restore) + + if repository.data.installed and not repository.content.single: + await hacs.hass.async_add_executor_job(backup.cleanup) + + if persistent_directory is not None: + await hacs.hass.async_add_executor_job(persistent_directory.restore) + await hacs.hass.async_add_executor_job(persistent_directory.cleanup) + + if repository.validate.success: + if repository.data.full_name not in repository.hacs.common.installed: + if repository.data.full_name == "hacs/integration": + repository.hacs.common.installed.append(repository.data.full_name) + repository.data.installed = True + repository.data.installed_commit = repository.data.last_commit + + if version == repository.data.default_branch: + repository.data.installed_version = None + else: + repository.data.installed_version = version diff --git a/custom_components/hacs/helpers/methods/registration.py b/custom_components/hacs/helpers/methods/registration.py new file mode 100644 index 0000000..d6f2288 --- /dev/null +++ b/custom_components/hacs/helpers/methods/registration.py @@ -0,0 +1,43 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member, attribute-defined-outside-init +from abc import ABC + +from custom_components.hacs.validate import async_run_repository_checks + + +class RepositoryMethodPreRegistration(ABC): + async def async_pre_registration(self): + pass + + +class RepositoryMethodRegistration(ABC): + async def registration(self, ref=None) -> None: + self.logger.warning( + "'registration' is deprecated, use 'async_registration' instead" + ) + await self.async_registration(ref) + + async def async_registration(self, ref=None) -> None: + # Run local pre registration steps. + await self.async_pre_registration() + + if ref is not None: + self.data.selected_tag = ref + self.ref = ref + self.force_branch = True + + if not await self.validate_repository(): + return False + + # Run common registration steps. + await self.common_registration() + + # Set correct local path + self.content.path.local = self.localpath + + # Run local post registration steps. + await self.async_post_registration() + + +class RepositoryMethodPostRegistration(ABC): + async def async_post_registration(self): + await async_run_repository_checks(self) diff --git a/custom_components/hacs/helpers/methods/reinstall_if_needed.py b/custom_components/hacs/helpers/methods/reinstall_if_needed.py new file mode 100644 index 0000000..a83353b --- /dev/null +++ b/custom_components/hacs/helpers/methods/reinstall_if_needed.py @@ -0,0 +1,12 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from abc import ABC + +from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist + + +class RepositoryMethodReinstallIfNeeded(ABC): + async def async_reinstall_if_needed(self) -> None: + if self.data.installed: + if not await async_path_exsist(self.content.path.local): + self.logger.error("Missing from local FS, should be reinstalled.") + # await self.async_install() diff --git a/custom_components/hacs/helpers/network.py b/custom_components/hacs/helpers/network.py deleted file mode 100644 index acc0a1f..0000000 --- a/custom_components/hacs/helpers/network.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Verify network.""" -from socket import gaierror -from integrationhelper import Logger - - -def internet_connectivity_check(host="api.github.com"): - """Verify network connectivity.""" - return True diff --git a/custom_components/hacs/helpers/properties/__init__.py b/custom_components/hacs/helpers/properties/__init__.py new file mode 100644 index 0000000..4267caf --- /dev/null +++ b/custom_components/hacs/helpers/properties/__init__.py @@ -0,0 +1,16 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from custom_components.hacs.helpers.properties.can_be_installed import ( + RepositoryPropertyCanBeInstalled, +) +from custom_components.hacs.helpers.properties.custom import RepositoryPropertyCustom +from custom_components.hacs.helpers.properties.pending_update import ( + RepositoryPropertyPendingUpdate, +) + + +class RepositoryHelperProperties( + RepositoryPropertyPendingUpdate, + RepositoryPropertyCustom, + RepositoryPropertyCanBeInstalled, +): + pass diff --git a/custom_components/hacs/helpers/properties/can_be_installed.py b/custom_components/hacs/helpers/properties/can_be_installed.py new file mode 100644 index 0000000..df6b4d6 --- /dev/null +++ b/custom_components/hacs/helpers/properties/can_be_installed.py @@ -0,0 +1,21 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from abc import ABC + +from custom_components.hacs.helpers.functions.misc import version_left_higher_then_right + + +class RepositoryPropertyCanBeInstalled(ABC): + @property + 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 + ): + return False + return True + + @property + def can_install(self): + """kept for legacy compatibility""" + return self.can_be_installed diff --git a/custom_components/hacs/helpers/properties/custom.py b/custom_components/hacs/helpers/properties/custom.py new file mode 100644 index 0000000..3cb0486 --- /dev/null +++ b/custom_components/hacs/helpers/properties/custom.py @@ -0,0 +1,13 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from abc import ABC + + +class RepositoryPropertyCustom(ABC): + @property + def custom(self): + """Return flag if the repository is custom.""" + if str(self.data.id) in self.hacs.common.default: + return False + if self.data.full_name == "hacs/integration": + return False + return True diff --git a/custom_components/hacs/helpers/properties/pending_update.py b/custom_components/hacs/helpers/properties/pending_update.py new file mode 100644 index 0000000..083930e --- /dev/null +++ b/custom_components/hacs/helpers/properties/pending_update.py @@ -0,0 +1,23 @@ +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member +from abc import ABC + + +class RepositoryPropertyPendingUpdate(ABC): + @property + def pending_update(self) -> bool: + if not self.can_install: + return False + if self.data.installed: + if self.data.selected_tag is not None: + if self.data.selected_tag == self.data.default_branch: + if self.data.installed_commit != self.data.last_commit: + return True + return False + if self.display_installed_version != self.display_available_version: + return True + return False + + @property + def pending_upgrade(self) -> bool: + """kept for legacy compatibility""" + return self.pending_update diff --git a/custom_components/hacs/helpers/register_repository.py b/custom_components/hacs/helpers/register_repository.py deleted file mode 100644 index d7c556f..0000000 --- a/custom_components/hacs/helpers/register_repository.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Register a repository.""" -from aiogithubapi import AIOGitHubException -from custom_components.hacs.globals import get_hacs -from custom_components.hacs.hacsbase.exceptions import ( - HacsException, - HacsExpectedException, -) - - -async def register_repository(full_name, category, check=True): - """Register a repository.""" - hacs = get_hacs() - from custom_components.hacs.repositories import ( - RERPOSITORY_CLASSES, - ) # To hanle import error - - if full_name in hacs.common.skip: - if full_name != "hacs/integration": - raise HacsExpectedException(f"Skipping {full_name}") - - if category not in RERPOSITORY_CLASSES: - raise HacsException(f"{category} is not a valid repository category.") - - repository = RERPOSITORY_CLASSES[category](full_name) - if check: - try: - await repository.registration() - if hacs.system.status.new: - repository.status.new = False - if repository.validate.errors: - hacs.common.skip.append(repository.data.full_name) - if not hacs.system.status.startup: - hacs.logger.error(f"Validation for {full_name} failed.") - return repository.validate.errors - repository.logger.info("Registration complete") - except AIOGitHubException as exception: - hacs.common.skip.append(repository.data.full_name) - raise HacsException(f"Validation for {full_name} failed with {exception}.") - - hacs.hass.bus.async_fire( - "hacs/repository", - { - "id": 1337, - "action": "registration", - "repository": repository.data.full_name, - "repository_id": repository.information.uid, - }, - ) - hacs.repositories.append(repository) diff --git a/custom_components/hacs/helpers/validate_repository.py b/custom_components/hacs/helpers/validate_repository.py deleted file mode 100644 index f20adbf..0000000 --- a/custom_components/hacs/helpers/validate_repository.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Helper to do common validation for repositories.""" -from aiogithubapi import AIOGitHubException -from custom_components.hacs.globals import get_hacs, is_removed -from custom_components.hacs.hacsbase.exceptions import HacsException -from custom_components.hacs.helpers.install import version_to_install -from custom_components.hacs.helpers.information import ( - get_repository, - get_tree, - get_releases, -) - - -async def common_validate(repository): - """Common validation steps of the repository.""" - repository.validate.errors = [] - - # Make sure the repository exist. - repository.logger.debug("Checking repository.") - await common_update_data(repository) - - # Step 6: Get the content of hacs.json - await repository.get_repository_manifest_content() - - -async def common_update_data(repository): - """Common update data.""" - hacs = get_hacs() - try: - repository_object = await get_repository( - hacs.session, hacs.configuration.token, repository.data.full_name - ) - repository.repository_object = repository_object - repository.data.update_data(repository_object.attributes) - except (AIOGitHubException, HacsException) as exception: - if not hacs.system.status.startup: - repository.logger.error(exception) - repository.validate.errors.append("Repository does not exist.") - raise HacsException(exception) - - # Make sure the repository is not archived. - if repository.data.archived: - repository.validate.errors.append("Repository is archived.") - raise HacsException("Repository is archived.") - - # Make sure the repository is not in the blacklist. - if is_removed(repository.data.full_name): - repository.validate.errors.append("Repository is in the blacklist.") - raise HacsException("Repository is in the blacklist.") - - # Get releases. - try: - releases = await get_releases( - repository.repository_object, - repository.status.show_beta, - hacs.configuration.release_limit, - ) - if releases: - repository.releases.releases = True - repository.releases.objects = releases - repository.releases.published_tags = [ - x.tag_name for x in releases if not x.draft - ] - repository.versions.available = next(iter(releases)).tag_name - for release in releases: - if release.tag_name == repository.ref: - assets = release.assets - if assets: - downloads = next(iter(assets)).attributes.get("download_count") - repository.releases.downloads = downloads - - except (AIOGitHubException, HacsException): - repository.releases.releases = False - - repository.ref = version_to_install(repository) - - repository.logger.debug( - f"Running checks against {repository.ref.replace('tags/', '')}" - ) - - try: - repository.tree = await get_tree(repository.repository_object, repository.ref) - if not repository.tree: - raise HacsException("No files in tree") - repository.treefiles = [] - for treefile in repository.tree: - repository.treefiles.append(treefile.full_path) - except (AIOGitHubException, HacsException) as exception: - if not hacs.system.status.startup: - repository.logger.error(exception) - raise HacsException(exception) diff --git a/custom_components/hacs/http.py b/custom_components/hacs/http.py deleted file mode 100644 index cab3fb1..0000000 --- a/custom_components/hacs/http.py +++ /dev/null @@ -1,83 +0,0 @@ -"""HACS http endpoints.""" -import os -from integrationhelper import Logger -from homeassistant.components.http import HomeAssistantView -from aiohttp import web -from hacs_frontend import locate_gz, locate_debug_gz - -from custom_components.hacs.globals import get_hacs - - -class HacsFrontend(HomeAssistantView): - """Base View Class for HACS.""" - - requires_auth = False - name = "hacs_files" - url = r"/hacsfiles/{requested_file:.+}" - - async def get(self, request, requested_file): # pylint: disable=unused-argument - """Handle HACS Web requests.""" - return await get_file_response(requested_file) - - -class HacsPluginViewLegacy(HacsFrontend): - """Alias for legacy, remove with 1.0""" - - name = "community_plugin" - url = r"/community_plugin/{requested_file:.+}" - - async def get(self, request, requested_file): # pylint: disable=unused-argument - """DEPRECATED.""" - hacs = get_hacs() - if hacs.system.ha_version.split(".")[1] >= "107": - logger = Logger("hacs.deprecated") - logger.warning( - "The '/community_plugin/*' is deprecated and will be removed in an upcomming version of HACS, it has been replaced by '/hacsfiles/*', if you use the UI to manage your lovelace configuration, you can update this by going to the settings tab in HACS, if you use YAML to manage your lovelace configuration, you manually need to replace the URL in your resources." - ) - - return await get_file_response(requested_file) - - -async def get_file_response(requested_file): - """Get file.""" - hacs = get_hacs() - - if requested_file.startswith("frontend-"): - if hacs.configuration.debug: - servefile = await hacs.hass.async_add_executor_job(locate_debug_gz) - hacs.logger.debug("Serving DEBUG frontend") - else: - servefile = await hacs.hass.async_add_executor_job(locate_gz) - - if os.path.exists(servefile): - return web.FileResponse(servefile) - elif requested_file == "iconset.js": - return web.FileResponse( - f"{hacs.system.config_path}/custom_components/hacs/iconset.js" - ) - - try: - if requested_file.startswith("themes"): - file = f"{hacs.system.config_path}/{requested_file}" - else: - file = f"{hacs.system.config_path}/www/community/{requested_file}" - - # Serve .gz if it exist - if os.path.exists(file + ".gz"): - file += ".gz" - - if os.path.exists(file): - hacs.logger.debug("Serving {} from {}".format(requested_file, file)) - response = web.FileResponse(file) - response.headers["Cache-Control"] = "no-store, max-age=0" - response.headers["Pragma"] = "no-store" - return response - else: - hacs.logger.error(f"Tried to serve up '{file}' but it does not exist") - - except Exception as error: # pylint: disable=broad-except - hacs.logger.debug( - "there was an issue trying to serve {} - {}".format(requested_file, error) - ) - - return web.Response(status=404) diff --git a/custom_components/hacs/iconset.js b/custom_components/hacs/iconset.js index ab225c7..ef425f2 100644 --- a/custom_components/hacs/iconset.js +++ b/custom_components/hacs/iconset.js @@ -1,34 +1,7 @@ -const iconset = document.createElement("ha-iconset-svg"); -iconset.name = "hacs"; -iconset.size = "1024"; -iconset.innerHTML = ` - - - - - - - - - - - - - - - -` -document.body.appendChild(iconset); \ No newline at end of file +window.customIconsets = window.customIconsets || {}; +window.customIconsets["hacs"] = async () => { + return { + path: + "m 20.064849,22.306912 c -0.0319,0.369835 -0.280561,0.707789 -0.656773,0.918212 -0.280572,0.153036 -0.605773,0.229553 -0.950094,0.229553 -0.0765,0 -0.146661,-0.0064 -0.216801,-0.01275 -0.605774,-0.05739 -1.135016,-0.344329 -1.402827,-0.7588 l 0.784304,-0.516495 c 0.0893,0.146659 0.344331,0.312448 0.707793,0.34433 0.235931,0.02551 0.471852,-0.01913 0.637643,-0.108401 0.101998,-0.05101 0.172171,-0.127529 0.17854,-0.191295 0.0065,-0.08289 -0.0255,-0.369835 -0.733293,-0.439975 -1.013854,-0.09565 -1.645127,-0.688661 -1.568606,-1.460214 0.0319,-0.382589 0.280561,-0.714165 0.663153,-0.930965 0.331571,-0.172165 0.752423,-0.25506 1.166895,-0.210424 0.599382,0.05739 1.128635,0.344329 1.402816,0.7588 l -0.784304,0.510118 c -0.0893,-0.140282 -0.344331,-0.299694 -0.707782,-0.331576 -0.235932,-0.02551 -0.471863,0.01913 -0.637654,0.10202 -0.0956,0.05739 -0.165791,0.133906 -0.17216,0.191295 -0.0255,0.293317 0.465482,0.420847 0.726913,0.439976 v 0.0064 c 1.020234,0.09565 1.638757,0.66953 1.562237,1.460213 z m -7.466854,-0.988354 c 0,-1.192401 0.962855,-2.155249 2.15525,-2.155249 0.599393,0 1.179645,0.25506 1.594117,0.707789 l -0.695033,0.624895 c -0.235931,-0.25506 -0.561133,-0.401718 -0.899084,-0.401718 -0.675903,0 -1.217906,0.542 -1.217906,1.217906 0,0.66953 0.542003,1.217908 1.217906,1.217908 0.337951,0 0.663153,-0.140283 0.899084,-0.401718 l 0.695033,0.631271 c -0.414472,0.452729 -0.988355,0.707788 -1.594117,0.707788 -1.192395,0 -2.15525,-0.969224 -2.15525,-2.148872 z M 8.6573365,23.461054 10.353474,19.14418 h 0.624893 l 1.568618,4.316874 H 11.52037 L 11.265308,22.734136 H 9.964513 l -0.274192,0.726918 z m 1.6833885,-1.68339 h 0.580263 L 10.646796,21.012487 Z M 8.1089536,19.156932 v 4.297745 H 7.1461095 v -1.645131 h -1.606867 v 1.645131 H 4.5763876 v -4.297745 h 0.9628549 v 1.696143 h 1.606867 V 19.156932 Z M 20.115859,4.2997436 C 20.090359,4.159461 19.969198,4.0574375 19.822548,4.0574375 H 14.141102 10.506516 4.8250686 c -0.14665,0 -0.2678112,0.1020202 -0.2933108,0.2423061 L 3.690064,8.8461703 c -0.00651,0.01913 -0.00651,0.03826 -0.00651,0.057391 v 1.5239797 c 0,0.165789 0.133911,0.299694 0.2996911,0.299694 H 4.5762579 20.0711 20.664112 c 0.165781,0 0.299691,-0.133905 0.299691,-0.299694 V 8.8971848 c 0,-0.01913 0,-0.03826 -0.0065,-0.05739 z M 4.5763876,17.358767 c 0,0.184917 0.1466608,0.331577 0.3315819,0.331577 h 5.5985465 3.634586 0.924594 c 0.184911,0 0.331571,-0.14666 0.331571,-0.331577 v -4.744098 c 0,-0.184918 0.146661,-0.331577 0.331582,-0.331577 h 2.894913 c 0.184921,0 0.331582,0.146659 0.331582,0.331577 v 4.744098 c 0,0.184917 0.146661,0.331577 0.331571,0.331577 h 0.446363 c 0.18491,0 0.331571,-0.14666 0.331571,-0.331577 v -5.636804 c 0,-0.184918 -0.146661,-0.331577 -0.331571,-0.331577 H 4.9079695 c -0.1849211,0 -0.3315819,0.146659 -0.3315819,0.331577 z m 1.6578879,-4.852498 h 5.6495565 c 0.15303,0 0.280561,0.12753 0.280561,0.280564 v 3.513438 c 0,0.153036 -0.127531,0.280566 -0.280561,0.280566 H 6.2342755 c -0.1530412,0 -0.2805719,-0.12753 -0.2805719,-0.280566 v -3.513438 c 0,-0.159411 0.1275307,-0.280564 0.2805719,-0.280564 z M 19.790657,3.3879075 H 4.8569594 c -0.1530412,0 -0.2805718,-0.1275296 -0.2805718,-0.2805642 V 1.3665653 C 4.5763876,1.2135296 4.7039182,1.086 4.8569594,1.086 H 19.790657 c 0.153041,0 0.280572,0.1275296 0.280572,0.2805653 v 1.740778 c 0,0.1530346 -0.127531,0.2805642 -0.280572,0.2805642 z", + }; +}; diff --git a/custom_components/hacs/manifest.json b/custom_components/hacs/manifest.json index 95c49e0..32ae63d 100644 --- a/custom_components/hacs/manifest.json +++ b/custom_components/hacs/manifest.json @@ -4,6 +4,7 @@ ], "config_flow": true, "dependencies": [ + "http", "websocket_api", "frontend", "persistent_notification", @@ -11,14 +12,16 @@ ], "documentation": "https://hacs.xyz/docs/configuration/start", "domain": "hacs", - "issues": "https://hacs.xyz/docs/issues", - "name": "HACS (Home Assistant Community Store)", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/hacs/integration/issues", + "name": "HACS", "requirements": [ - "aiofiles==0.4.0", - "aiogithubapi==0.5.0", - "backoff==1.10.0", - "hacs_frontend==20200309184730", - "integrationhelper==0.2.2", - "semantic_version==2.8.4" - ] + "aiofiles>=0.6.0", + "aiogithubapi>=21.4.0", + "awesomeversion>=21.2.2", + "backoff>=1.10.0", + "hacs_frontend==20210620205902", + "queueman==0.5" + ], + "version": "1.13.2" } \ No newline at end of file diff --git a/custom_components/hacs/models/__init__.py b/custom_components/hacs/models/__init__.py new file mode 100644 index 0000000..3fb384a --- /dev/null +++ b/custom_components/hacs/models/__init__.py @@ -0,0 +1 @@ +"""Hacs models.""" diff --git a/custom_components/hacs/models/core.py b/custom_components/hacs/models/core.py new file mode 100644 index 0000000..7e2c94e --- /dev/null +++ b/custom_components/hacs/models/core.py @@ -0,0 +1,15 @@ +"""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("yaml") diff --git a/custom_components/hacs/models/frontend.py b/custom_components/hacs/models/frontend.py new file mode 100644 index 0000000..1b9d875 --- /dev/null +++ b/custom_components/hacs/models/frontend.py @@ -0,0 +1,10 @@ +"""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 new file mode 100644 index 0000000..4923c62 --- /dev/null +++ b/custom_components/hacs/models/system.py @@ -0,0 +1,18 @@ +"""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/customisations/globs/.gitkeep b/custom_components/hacs/operational/__init__.py similarity index 100% rename from customisations/globs/.gitkeep rename to custom_components/hacs/operational/__init__.py diff --git a/custom_components/hacs/hacsbase/backup.py b/custom_components/hacs/operational/backup.py similarity index 81% rename from custom_components/hacs/hacsbase/backup.py rename to custom_components/hacs/operational/backup.py index acd20c1..4edf8eb 100644 --- a/custom_components/hacs/hacsbase/backup.py +++ b/custom_components/hacs/operational/backup.py @@ -1,117 +1,124 @@ -"""Backup.""" -import os -import shutil -import tempfile -from time import sleep - -from integrationhelper import Logger - -BACKUP_PATH = tempfile.gettempdir() + "/hacs_backup/" - - -class Backup: - """Backup.""" - - def __init__(self, local_path, backup_path=BACKUP_PATH): - """initialize.""" - self.logger = Logger("hacs.backup") - self.local_path = local_path - self.backup_path = backup_path - self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}" - - def create(self): - """Create a backup in /tmp""" - if not os.path.exists(self.local_path): - return - if os.path.exists(self.backup_path): - shutil.rmtree(self.backup_path) - while os.path.exists(self.backup_path): - sleep(0.1) - os.makedirs(self.backup_path, exist_ok=True) - - try: - if os.path.isfile(self.local_path): - shutil.copyfile(self.local_path, self.backup_path_full) - os.remove(self.local_path) - else: - shutil.copytree(self.local_path, self.backup_path_full) - shutil.rmtree(self.local_path) - while os.path.exists(self.local_path): - sleep(0.1) - self.logger.debug( - f"Backup for {self.local_path}, created in {self.backup_path_full}" - ) - except Exception: # pylint: disable=broad-except - pass - - def restore(self): - """Restore from backup.""" - if not os.path.exists(self.backup_path_full): - return - - if os.path.isfile(self.backup_path_full): - if os.path.exists(self.local_path): - os.remove(self.local_path) - shutil.copyfile(self.backup_path_full, self.local_path) - else: - if os.path.exists(self.local_path): - shutil.rmtree(self.local_path) - while os.path.exists(self.local_path): - sleep(0.1) - shutil.copytree(self.backup_path_full, self.local_path) - self.logger.debug( - f"Restored {self.local_path}, from backup {self.backup_path_full}" - ) - - def cleanup(self): - """Cleanup backup files.""" - if os.path.exists(self.backup_path): - shutil.rmtree(self.backup_path) - while os.path.exists(self.backup_path): - sleep(0.1) - self.logger.debug(f"Backup dir {self.backup_path} cleared") - - -class BackupNetDaemon: - """BackupNetDaemon.""" - - def __init__(self, repository): - """Initialize.""" - self.repository = repository - self.logger = Logger("hacs.backup") - self.backup_path = ( - tempfile.gettempdir() + "/hacs_persistent_netdaemon/" + repository.data.name - ) - - def create(self): - """Create a backup in /tmp""" - if os.path.exists(self.backup_path): - shutil.rmtree(self.backup_path) - while os.path.exists(self.backup_path): - sleep(0.1) - os.makedirs(self.backup_path, exist_ok=True) - - for filename in os.listdir(self.repository.content.path.local): - if filename.endswith(".yaml"): - source_file_name = f"{self.repository.content.path.local}/{filename}" - target_file_name = f"{self.backup_path}/{filename}" - shutil.copyfile(source_file_name, target_file_name) - - def restore(self): - """Create a backup in /tmp""" - if os.path.exists(self.backup_path): - for filename in os.listdir(self.backup_path): - if filename.endswith(".yaml"): - source_file_name = f"{self.backup_path}/{filename}" - target_file_name = ( - f"{self.repository.content.path.local}/{filename}" - ) - shutil.copyfile(source_file_name, target_file_name) - - def cleanup(self): - """Create a backup in /tmp""" - if os.path.exists(self.backup_path): - shutil.rmtree(self.backup_path) - while os.path.exists(self.backup_path): - sleep(0.1) - self.logger.debug(f"Backup dir {self.backup_path} cleared") +"""Backup.""" +import os +import shutil +import tempfile +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 + +BACKUP_PATH = tempfile.gettempdir() + "/hacs_backup/" + +_LOGGER = getLogger() + + +class Backup: + """Backup.""" + + def __init__(self, local_path, backup_path=BACKUP_PATH): + """initialize.""" + self.local_path = local_path + self.backup_path = backup_path + self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}" + + def create(self): + """Create a backup in /tmp""" + if not os.path.exists(self.local_path): + return + if not is_safe_to_remove(self.local_path): + return + if os.path.exists(self.backup_path): + shutil.rmtree(self.backup_path) + while os.path.exists(self.backup_path): + sleep(0.1) + os.makedirs(self.backup_path, exist_ok=True) + + try: + if os.path.isfile(self.local_path): + shutil.copyfile(self.local_path, self.backup_path_full) + os.remove(self.local_path) + else: + shutil.copytree(self.local_path, self.backup_path_full) + shutil.rmtree(self.local_path) + while os.path.exists(self.local_path): + sleep(0.1) + _LOGGER.debug( + "Backup for %s, created in %s", + self.local_path, + self.backup_path_full, + ) + except (Exception, BaseException): # pylint: disable=broad-except + pass + + def restore(self): + """Restore from backup.""" + if not os.path.exists(self.backup_path_full): + return + + if os.path.isfile(self.backup_path_full): + if os.path.exists(self.local_path): + os.remove(self.local_path) + shutil.copyfile(self.backup_path_full, self.local_path) + else: + if os.path.exists(self.local_path): + shutil.rmtree(self.local_path) + 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 + ) + + def cleanup(self): + """Cleanup backup files.""" + if os.path.exists(self.backup_path): + shutil.rmtree(self.backup_path) + while os.path.exists(self.backup_path): + sleep(0.1) + _LOGGER.debug("Backup dir %s cleared", self.backup_path) + + +class BackupNetDaemon: + """BackupNetDaemon.""" + + def __init__(self, repository): + """Initialize.""" + self.repository = repository + self.backup_path = ( + tempfile.gettempdir() + "/hacs_persistent_netdaemon/" + repository.data.name + ) + + def create(self): + """Create a backup in /tmp""" + if not is_safe_to_remove(self.repository.content.path.local): + return + if os.path.exists(self.backup_path): + shutil.rmtree(self.backup_path) + while os.path.exists(self.backup_path): + sleep(0.1) + os.makedirs(self.backup_path, exist_ok=True) + + for filename in os.listdir(self.repository.content.path.local): + if filename.endswith(".yaml"): + source_file_name = f"{self.repository.content.path.local}/{filename}" + target_file_name = f"{self.backup_path}/{filename}" + shutil.copyfile(source_file_name, target_file_name) + + def restore(self): + """Create a backup in /tmp""" + if os.path.exists(self.backup_path): + for filename in os.listdir(self.backup_path): + if filename.endswith(".yaml"): + source_file_name = f"{self.backup_path}/{filename}" + target_file_name = ( + f"{self.repository.content.path.local}/{filename}" + ) + shutil.copyfile(source_file_name, target_file_name) + + def cleanup(self): + """Create a backup in /tmp""" + if os.path.exists(self.backup_path): + shutil.rmtree(self.backup_path) + while os.path.exists(self.backup_path): + sleep(0.1) + _LOGGER.debug("Backup dir %s cleared", self.backup_path) diff --git a/custom_components/hacs/operational/factory.py b/custom_components/hacs/operational/factory.py new file mode 100644 index 0000000..f40de66 --- /dev/null +++ b/custom_components/hacs/operational/factory.py @@ -0,0 +1,61 @@ +# pylint: disable=missing-docstring,invalid-name +import asyncio + +from aiogithubapi import AIOGitHubAPIException + +from custom_components.hacs.helpers.classes.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, +) + +max_concurrent_tasks = asyncio.Semaphore(15) +sleeper = 5 + +_LOGGER = getLogger() + + +class HacsTaskFactory: + def __init__(self): + self.tasks = [] + self.running = False + + async def safe_common_update(self, repository): + async with max_concurrent_tasks: + try: + await repository.common_update() + except HacsNotModifiedException: + pass + except (AIOGitHubAPIException, HacsException) as exception: + _LOGGER.error("%s - %s", repository.data.full_name, exception) + + # Due to GitHub ratelimits we need to sleep a bit + await asyncio.sleep(sleeper) + + async def safe_update(self, repository): + async with max_concurrent_tasks: + try: + await repository.update_repository() + except HacsNotModifiedException: + pass + except HacsRepositoryArchivedException as exception: + _LOGGER.warning("%s - %s", repository.data.full_name, exception) + except (AIOGitHubAPIException, HacsException) as exception: + _LOGGER.error("%s - %s", repository.data.full_name, exception) + + # Due to GitHub ratelimits we need to sleep a bit + await asyncio.sleep(sleeper) + + async def safe_register(self, repo, category): + async with max_concurrent_tasks: + try: + await register_repository(repo, category) + except (AIOGitHubAPIException, HacsException) as exception: + _LOGGER.error("%s - %s", repo, exception) + + # Due to GitHub ratelimits we need to sleep a bit + await asyncio.sleep(sleeper) diff --git a/custom_components/hacs/operational/reload.py b/custom_components/hacs/operational/reload.py new file mode 100644 index 0000000..037887e --- /dev/null +++ b/custom_components/hacs/operational/reload.py @@ -0,0 +1,10 @@ +"""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 new file mode 100644 index 0000000..799f9cf --- /dev/null +++ b/custom_components/hacs/operational/remove.py @@ -0,0 +1,34 @@ +"""Remove HACS.""" +from typing import TYPE_CHECKING +from ..const import DOMAIN +from ..enums import HacsDisabledReason +from ..share import get_hacs + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.config_entries import ConfigEntry + + +async def async_remove_entry(hass: "HomeAssistant", config_entry: "ConfigEntry"): + """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 str(config_entry.state) in ["ConfigEntryState.LOADED", "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 new file mode 100644 index 0000000..95f5985 --- /dev/null +++ b/custom_components/hacs/operational/runtime.py @@ -0,0 +1 @@ +"""Runtime...""" diff --git a/custom_components/hacs/operational/setup.py b/custom_components/hacs/operational/setup.py new file mode 100644 index 0000000..7bc4536 --- /dev/null +++ b/custom_components/hacs/operational/setup.py @@ -0,0 +1,218 @@ +"""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 homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.event import async_call_later + +from custom_components.hacs.const import ( + DOMAIN, + HACS_GITHUB_API_HEADERS, + INTEGRATION_VERSION, + STARTUP, +) +from custom_components.hacs.enums import HacsDisabledReason, HacsStage, LovelaceMode +from custom_components.hacs.hacsbase.configuration import Configuration +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 + +try: + from homeassistant.components.lovelace import system_health_info +except ImportError: + from homeassistant.components.lovelace.system_health import system_health_info + + +async def _async_common_setup(hass): + """Common setup stages.""" + hacs = get_hacs() + hacs.hass = hass + hacs.system.running = True + hacs.session = async_create_clientsession(hass) + + +async def async_setup_entry(hass, config_entry): + """Set up this integration using UI.""" + from homeassistant import config_entries + + hacs = get_hacs() + if hass.data.get(DOMAIN) is not None: + return False + if config_entry.source == config_entries.SOURCE_IMPORT: + hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) + return False + + await _async_common_setup(hass) + + hacs.configuration = Configuration.from_dict( + config_entry.data, config_entry.options + ) + hacs.configuration.config_type = "flow" + hacs.configuration.config_entry = config_entry + + return await async_startup_wrapper_for_config_entry() + + +async def async_setup(hass, config): + """Set up this integration using yaml.""" + hacs = get_hacs() + if DOMAIN not in config: + return True + if hacs.configuration and hacs.configuration.config_type == "flow": + return True + + await _async_common_setup(hass) + + hacs.configuration = Configuration.from_dict(config[DOMAIN]) + hacs.configuration.config_type = "yaml" + await async_startup_wrapper_for_yaml() + return True + + +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: + startup_result = False + if not startup_result: + hacs.system.disabled = True + raise ConfigEntryNotReady + hacs.enable() + return startup_result + + +async def async_startup_wrapper_for_yaml(_=None): + """Startup wrapper for yaml config.""" + hacs = get_hacs() + try: + startup_result = await async_hacs_startup() + except AIOGitHubAPIException: + startup_result = False + if not startup_result: + hacs.system.disabled = True + 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() + + +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, 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.version = INTEGRATION_VERSION + hacs.log.info(STARTUP) + hacs.core.config_path = hacs.hass.config.path() + hacs.system.ha_version = HAVERSION + + hacs.system.lovelace_mode = lovelace_info.get("mode", "yaml") + hacs.core.lovelace_mode = LovelaceMode(lovelace_info.get("mode", "yaml")) + + # 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.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) + 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) + return False + + # Setup startup tasks + if hacs.hass.state == CoreState.running: + async_call_later(hacs.hass, 5, hacs.startup_tasks) + 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 diff --git a/entities/device_trackers/.gitkeep b/custom_components/hacs/operational/setup_actions/__init__.py similarity index 100% rename from entities/device_trackers/.gitkeep rename to custom_components/hacs/operational/setup_actions/__init__.py diff --git a/custom_components/hacs/operational/setup_actions/categories.py b/custom_components/hacs/operational/setup_actions/categories.py new file mode 100644 index 0000000..5092f66 --- /dev/null +++ b/custom_components/hacs/operational/setup_actions/categories.py @@ -0,0 +1,42 @@ +"""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 new file mode 100644 index 0000000..c67aaaf --- /dev/null +++ b/custom_components/hacs/operational/setup_actions/clear_storage.py @@ -0,0 +1,24 @@ +"""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 new file mode 100644 index 0000000..5ed38cd --- /dev/null +++ b/custom_components/hacs/operational/setup_actions/frontend.py @@ -0,0 +1,78 @@ +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 + use_cache = hacs.core.lovelace_mode == "storage" + hacs.log.info( + "%s mode, cache for /hacsfiles/: %s", + hacs.core.lovelace_mode, + use_cache, + ) + hass.http.register_static_path( + URL_BASE, + hass.config.path("www/community"), + cache_headers=use_cache, + ) + + 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": f"/hacsfiles/frontend/entrypoint.js?hacstag={FE_VERSION}", + } + }, + 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 new file mode 100644 index 0000000..0cce8e9 --- /dev/null +++ b/custom_components/hacs/operational/setup_actions/load_hacs_repository.py @@ -0,0 +1,38 @@ +"""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 new file mode 100644 index 0000000..13be265 --- /dev/null +++ b/custom_components/hacs/operational/setup_actions/sensor.py @@ -0,0 +1,25 @@ +""""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 new file mode 100644 index 0000000..cf3e5b3 --- /dev/null +++ b/custom_components/hacs/operational/setup_actions/websocket_api.py @@ -0,0 +1,36 @@ +"""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 ddd6003..91d89c4 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.theme import HacsTheme -from custom_components.hacs.repositories.integration import HacsIntegration -from custom_components.hacs.repositories.python_script import HacsPythonScript -from custom_components.hacs.repositories.appdaemon import HacsAppdaemon -from custom_components.hacs.repositories.netdaemon import HacsNetdaemon -from custom_components.hacs.repositories.plugin import HacsPlugin - -RERPOSITORY_CLASSES = { - "theme": HacsTheme, - "integration": HacsIntegration, - "python_script": HacsPythonScript, - "appdaemon": HacsAppdaemon, - "netdaemon": HacsNetdaemon, - "plugin": HacsPlugin, -} +"""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 + +RERPOSITORY_CLASSES = { + "theme": HacsTheme, + "integration": HacsIntegration, + "python_script": HacsPythonScript, + "appdaemon": HacsAppdaemon, + "netdaemon": HacsNetdaemon, + "plugin": HacsPlugin, +} diff --git a/custom_components/hacs/repositories/__pycache__/__init__.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index f1784fa..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/appdaemon.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/appdaemon.cpython-37.pyc deleted file mode 100644 index b6a4e1f..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/appdaemon.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/integration.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/integration.cpython-37.pyc deleted file mode 100644 index beeebf5..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/integration.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/manifest.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/manifest.cpython-37.pyc deleted file mode 100644 index d53053f..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/manifest.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/netdaemon.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/netdaemon.cpython-37.pyc deleted file mode 100644 index 2ead6c3..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/netdaemon.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/plugin.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/plugin.cpython-37.pyc deleted file mode 100644 index 51def82..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/plugin.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/python_script.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/python_script.cpython-37.pyc deleted file mode 100644 index 0802fa8..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/python_script.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/removed.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/removed.cpython-37.pyc deleted file mode 100644 index 62965b5..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/removed.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/repository.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/repository.cpython-37.pyc deleted file mode 100644 index eb03ac3..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/repository.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/repositorydata.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/repositorydata.cpython-37.pyc deleted file mode 100644 index fa45d66..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/repositorydata.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/__pycache__/theme.cpython-37.pyc b/custom_components/hacs/repositories/__pycache__/theme.cpython-37.pyc deleted file mode 100644 index a135f46..0000000 Binary files a/custom_components/hacs/repositories/__pycache__/theme.cpython-37.pyc and /dev/null differ diff --git a/custom_components/hacs/repositories/appdaemon.py b/custom_components/hacs/repositories/appdaemon.py index 92d19e4..c580b57 100644 --- a/custom_components/hacs/repositories/appdaemon.py +++ b/custom_components/hacs/repositories/appdaemon.py @@ -1,85 +1,73 @@ -"""Class for appdaemon apps in HACS.""" -from aiogithubapi import AIOGitHubException -from integrationhelper import Logger - -from .repository import HacsRepository -from ..hacsbase.exceptions import HacsException - - -class HacsAppdaemon(HacsRepository): - """Appdaemon apps in HACS.""" - - def __init__(self, full_name): - """Initialize.""" - super().__init__() - self.data.full_name = full_name - self.data.category = "appdaemon" - self.content.path.local = self.localpath - self.content.path.remote = "apps" - self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") - - @property - def localpath(self): - """Return localpath.""" - return f"{self.hacs.system.config_path}/appdaemon/apps/{self.data.name}" - - async def validate_repository(self): - """Validate.""" - await self.common_validate() - - # Custom step 1: Validate content. - try: - addir = await self.repository_object.get_contents("apps", self.ref) - except AIOGitHubException: - raise HacsException( - f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" - ) - - if not isinstance(addir, list): - self.validate.errors.append("Repostitory structure not compliant") - - self.content.path.remote = addir[0].path - self.content.objects = await self.repository_object.get_contents( - self.content.path.remote, self.ref - ) - - # Handle potential errors - if self.validate.errors: - for error in self.validate.errors: - if not self.hacs.system.status.startup: - self.logger.error(error) - return self.validate.success - - async def registration(self): - """Registration.""" - if not await self.validate_repository(): - return False - - # Run common registration steps. - await self.common_registration() - - # Set local path - self.content.path.local = self.localpath - - async def update_repository(self): - """Update.""" - if self.hacs.github.ratelimits.remaining == 0: - return - await self.common_update() - - # Get appdaemon objects. - if self.repository_manifest: - if self.data.content_in_root: - self.content.path.remote = "" - - if self.content.path.remote == "apps": - 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 - ) - - # Set local path - self.content.path.local = self.localpath +"""Class for appdaemon apps in HACS.""" +from aiogithubapi import AIOGitHubAPIException + +from custom_components.hacs.enums import HacsCategory +from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.helpers.classes.repository import HacsRepository + + +class HacsAppdaemon(HacsRepository): + """Appdaemon apps in HACS.""" + + def __init__(self, full_name): + """Initialize.""" + super().__init__() + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.APPDAEMON + self.content.path.local = self.localpath + self.content.path.remote = "apps" + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/appdaemon/apps/{self.data.name}" + + async def validate_repository(self): + """Validate.""" + await self.common_validate() + + # Custom step 1: Validate content. + try: + addir = await self.repository_object.get_contents("apps", self.ref) + except AIOGitHubAPIException: + raise HacsException( + f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" + ) from None + + if not isinstance(addir, list): + self.validate.errors.append("Repostitory structure not compliant") + + self.content.path.remote = addir[0].path + self.content.objects = await self.repository_object.get_contents( + self.content.path.remote, self.ref + ) + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self, error) + return self.validate.success + + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force): + return + + # Get appdaemon objects. + if self.repository_manifest: + if self.data.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "apps": + 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 + ) + + # Set local path + self.content.path.local = self.localpath diff --git a/custom_components/hacs/repositories/integration.py b/custom_components/hacs/repositories/integration.py index 7b9f874..3e3655e 100644 --- a/custom_components/hacs/repositories/integration.py +++ b/custom_components/hacs/repositories/integration.py @@ -1,93 +1,97 @@ -"""Class for integrations in HACS.""" -from integrationhelper import Logger - -from homeassistant.loader import async_get_custom_components - -from custom_components.hacs.hacsbase.exceptions import HacsException -from custom_components.hacs.helpers.filters import get_first_directory_in_directory -from custom_components.hacs.helpers.information import get_integration_manifest -from custom_components.hacs.repositories.repository import HacsRepository - - -class HacsIntegration(HacsRepository): - """Integrations in HACS.""" - - def __init__(self, full_name): - """Initialize.""" - super().__init__() - self.data.full_name = full_name - self.data.category = "integration" - self.content.path.remote = "custom_components" - self.content.path.local = self.localpath - self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") - - @property - def localpath(self): - """Return localpath.""" - return f"{self.hacs.system.config_path}/custom_components/{self.data.domain}" - - async def validate_repository(self): - """Validate.""" - await self.common_validate() - - # Custom step 1: Validate content. - if self.data.content_in_root: - self.content.path.remote = "" - - if self.content.path.remote == "custom_components": - name = get_first_directory_in_directory(self.tree, "custom_components") - if name is None: - raise HacsException( - f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" - ) - self.content.path.remote = f"custom_components/{name}" - - try: - await get_integration_manifest(self) - except HacsException as exception: - self.logger.error(exception) - - # Handle potential errors - if self.validate.errors: - for error in self.validate.errors: - if not self.hacs.system.status.startup: - self.logger.error(error) - return self.validate.success - - async def registration(self): - """Registration.""" - if not await self.validate_repository(): - return False - - # Run common registration steps. - await self.common_registration() - - # Set local path - self.content.path.local = self.localpath - - async def update_repository(self): - """Update.""" - if self.hacs.github.ratelimits.remaining == 0: - return - await self.common_update() - - if self.data.content_in_root: - self.content.path.remote = "" - - if self.content.path.remote == "custom_components": - name = get_first_directory_in_directory(self.tree, "custom_components") - self.content.path.remote = f"custom_components/{name}" - - try: - await get_integration_manifest(self) - except HacsException as exception: - self.logger.error(exception) - - # Set local path - self.content.path.local = self.localpath - - async def reload_custom_components(self): - """Reload custom_components (and config flows)in HA.""" - self.logger.info("Reloading custom_component cache") - del self.hacs.hass.data["custom_components"] - await async_get_custom_components(self.hacs.hass) +"""Class for integrations in HACS.""" +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.helpers.classes.repository import HacsRepository +from custom_components.hacs.helpers.functions.filters import ( + get_first_directory_in_directory, +) +from custom_components.hacs.helpers.functions.information import ( + get_integration_manifest, +) + + +class HacsIntegration(HacsRepository): + """Integrations in HACS.""" + + def __init__(self, full_name): + """Initialize.""" + super().__init__() + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.INTEGRATION + self.content.path.remote = "custom_components" + self.content.path.local = self.localpath + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/custom_components/{self.data.domain}" + + async def async_post_installation(self): + """Run post installation steps.""" + if self.data.config_flow: + if self.data.full_name != "hacs/integration": + await self.reload_custom_components() + if self.data.first_install: + self.pending_restart = False + return + self.pending_restart = True + + async def validate_repository(self): + """Validate.""" + await self.common_validate() + + # Custom step 1: Validate content. + if self.data.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "custom_components": + name = get_first_directory_in_directory(self.tree, "custom_components") + if name is None: + raise HacsException( + f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" + ) + self.content.path.remote = f"custom_components/{name}" + + try: + await get_integration_manifest(self) + except HacsException as exception: + if self.hacs.system.action: + raise HacsException(f"::error:: {exception}") from exception + self.logger.error("%s %s", self, exception) + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self, error) + return self.validate.success + + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force): + return + + if self.data.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "custom_components": + name = get_first_directory_in_directory(self.tree, "custom_components") + self.content.path.remote = f"custom_components/{name}" + + try: + await get_integration_manifest(self) + except HacsException as exception: + self.logger.error("%s %s", self, exception) + + # Set local path + self.content.path.local = self.localpath + + async def reload_custom_components(self): + """Reload custom_components (and config flows)in HA.""" + self.logger.info("Reloading custom_component cache") + del self.hacs.hass.data["custom_components"] + await async_get_custom_components(self.hacs.hass) + self.logger.info("Custom_component cache reloaded") diff --git a/custom_components/hacs/repositories/netdaemon.py b/custom_components/hacs/repositories/netdaemon.py index 5c3ed1e..45c73a8 100644 --- a/custom_components/hacs/repositories/netdaemon.py +++ b/custom_components/hacs/repositories/netdaemon.py @@ -1,90 +1,87 @@ -"""Class for netdaemon apps in HACS.""" -from integrationhelper import Logger - -from .repository import HacsRepository -from ..hacsbase.exceptions import HacsException - -from custom_components.hacs.helpers.filters import get_first_directory_in_directory - - -class HacsNetdaemon(HacsRepository): - """Netdaemon apps in HACS.""" - - def __init__(self, full_name): - """Initialize.""" - super().__init__() - self.data.full_name = full_name - self.data.category = "netdaemon" - self.content.path.local = self.localpath - self.content.path.remote = "apps" - self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") - - @property - def localpath(self): - """Return localpath.""" - return f"{self.hacs.system.config_path}/netdaemon/apps/{self.data.name}" - - async def validate_repository(self): - """Validate.""" - await self.common_validate() - - # Custom step 1: Validate content. - if self.repository_manifest: - if self.data.content_in_root: - self.content.path.remote = "" - - if self.content.path.remote == "apps": - self.data.domain = get_first_directory_in_directory( - self.tree, self.content.path.remote - ) - self.content.path.remote = f"apps/{self.data.name}" - - compliant = False - for treefile in self.treefiles: - if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( - ".cs" - ): - compliant = True - break - if not compliant: - raise HacsException( - f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" - ) - - # Handle potential errors - if self.validate.errors: - for error in self.validate.errors: - if not self.hacs.system.status.startup: - self.logger.error(error) - return self.validate.success - - async def registration(self): - """Registration.""" - if not await self.validate_repository(): - return False - - # Run common registration steps. - await self.common_registration() - - # Set local path - self.content.path.local = self.localpath - - async def update_repository(self): - """Update.""" - if self.hacs.github.ratelimits.remaining == 0: - return - await self.common_update() - - # Get appdaemon objects. - if self.repository_manifest: - if self.data.content_in_root: - self.content.path.remote = "" - - if self.content.path.remote == "apps": - self.data.domain = get_first_directory_in_directory( - self.tree, self.content.path.remote - ) - self.content.path.remote = f"apps/{self.data.name}" - - # Set local path - self.content.path.local = self.localpath +"""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.helpers.classes.repository import HacsRepository +from custom_components.hacs.helpers.functions.filters import ( + get_first_directory_in_directory, +) + + +class HacsNetdaemon(HacsRepository): + """Netdaemon apps in HACS.""" + + def __init__(self, full_name): + """Initialize.""" + super().__init__() + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.NETDAEMON + self.content.path.local = self.localpath + self.content.path.remote = "apps" + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/netdaemon/apps/{self.data.name}" + + async def validate_repository(self): + """Validate.""" + await self.common_validate() + + # Custom step 1: Validate content. + if self.repository_manifest: + if self.data.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "apps": + self.data.domain = get_first_directory_in_directory( + self.tree, self.content.path.remote + ) + self.content.path.remote = f"apps/{self.data.name}" + + compliant = False + for treefile in self.treefiles: + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( + ".cs" + ): + compliant = True + break + if not compliant: + raise HacsException( + f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" + ) + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self, error) + return self.validate.success + + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force): + return + + # Get appdaemon objects. + if self.repository_manifest: + if self.data.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "apps": + self.data.domain = get_first_directory_in_directory( + self.tree, self.content.path.remote + ) + self.content.path.remote = f"apps/{self.data.name}" + + # Set local path + self.content.path.local = self.localpath + + async def async_post_installation(self): + """Run post installation steps.""" + try: + await self.hacs.hass.services.async_call( + "hassio", "addon_restart", {"addon": "c6a2317c_netdaemon"} + ) + except (Exception, BaseException): # pylint: disable=broad-except + pass diff --git a/custom_components/hacs/repositories/plugin.py b/custom_components/hacs/repositories/plugin.py index 277e604..52b9fe2 100644 --- a/custom_components/hacs/repositories/plugin.py +++ b/custom_components/hacs/repositories/plugin.py @@ -1,107 +1,77 @@ -"""Class for plugins in HACS.""" -import json -from integrationhelper import Logger - -from .repository import HacsRepository -from ..hacsbase.exceptions import HacsException - -from custom_components.hacs.helpers.information import find_file_name - - -class HacsPlugin(HacsRepository): - """Plugins in HACS.""" - - def __init__(self, full_name): - """Initialize.""" - super().__init__() - self.data.full_name = full_name - self.data.file_name = None - self.data.category = "plugin" - self.information.javascript_type = None - self.content.path.local = ( - f"{self.hacs.system.config_path}/www/community/{full_name.split('/')[-1]}" - ) - self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") - - async def validate_repository(self): - """Validate.""" - # Run common validation steps. - await self.common_validate() - - # Custom step 1: Validate content. - find_file_name(self) - - if self.content.path.remote is None: - raise HacsException( - f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" - ) - - if self.content.path.remote == "release": - self.content.single = True - - # Handle potential errors - if self.validate.errors: - for error in self.validate.errors: - if not self.hacs.system.status.startup: - self.logger.error(error) - return self.validate.success - - async def registration(self): - """Registration.""" - if not await self.validate_repository(): - return False - - # Run common registration steps. - await self.common_registration() - - async def update_repository(self): - """Update.""" - if self.hacs.github.ratelimits.remaining == 0: - return - # Run common update steps. - await self.common_update() - - # Get plugin objects. - find_file_name(self) - - # Get JS type - await self.parse_readme_for_jstype() - - if self.content.path.remote is None: - self.validate.errors.append("Repostitory structure not compliant") - - if self.content.path.remote == "release": - self.content.single = True - - async def get_package_content(self): - """Get package content.""" - try: - package = await self.repository_object.get_contents("package.json") - package = json.loads(package.content) - - if package: - self.data.authors = package["author"] - except Exception: # pylint: disable=broad-except - pass - - async def parse_readme_for_jstype(self): - """Parse the readme looking for js type.""" - readme = None - readme_files = ["readme", "readme.md"] - root = await self.repository_object.get_contents("") - for file in root: - if file.name.lower() in readme_files: - readme = await self.repository_object.get_contents(file.name) - break - - if readme is None: - return - - readme = readme.content - for line in readme.splitlines(): - if "type: module" in line: - self.information.javascript_type = "module" - break - elif "type: js" in line: - self.information.javascript_type = "js" - break +"""Class for plugins in HACS.""" +import json + +from custom_components.hacs.helpers.classes.exceptions import HacsException +from custom_components.hacs.helpers.classes.repository import HacsRepository +from custom_components.hacs.helpers.functions.information import find_file_name + + +class HacsPlugin(HacsRepository): + """Plugins in HACS.""" + + def __init__(self, full_name): + """Initialize.""" + super().__init__() + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.file_name = None + self.data.category = "plugin" + self.information.javascript_type = None + self.content.path.local = self.localpath + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/www/community/{self.data.full_name.split('/')[-1]}" + + async def validate_repository(self): + """Validate.""" + # Run common validation steps. + await self.common_validate() + + # Custom step 1: Validate content. + find_file_name(self) + + if self.content.path.remote is None: + raise HacsException( + f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" + ) + + if self.content.path.remote == "release": + self.content.single = True + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self, error) + return self.validate.success + + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force): + return + + # Get plugin objects. + find_file_name(self) + + if self.content.path.remote is None: + self.validate.errors.append( + f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" + ) + + if self.content.path.remote == "release": + self.content.single = True + + async def get_package_content(self): + """Get package content.""" + try: + package = await self.repository_object.get_contents( + "package.json", self.ref + ) + package = json.loads(package.content) + + if package: + self.data.authors = package["author"] + except (Exception, BaseException): # pylint: disable=broad-except + pass diff --git a/custom_components/hacs/repositories/python_script.py b/custom_components/hacs/repositories/python_script.py index c9f2db7..9741397 100644 --- a/custom_components/hacs/repositories/python_script.py +++ b/custom_components/hacs/repositories/python_script.py @@ -1,87 +1,83 @@ -"""Class for python_scripts in HACS.""" -from integrationhelper import Logger - -from .repository import HacsRepository -from ..hacsbase.exceptions import HacsException -from ..helpers.information import find_file_name - - -class HacsPythonScript(HacsRepository): - """python_scripts in HACS.""" - - category = "python_script" - - def __init__(self, full_name): - """Initialize.""" - super().__init__() - self.data.full_name = full_name - self.data.category = "python_script" - self.content.path.remote = "python_scripts" - self.content.path.local = f"{self.hacs.system.config_path}/python_scripts" - self.content.single = True - self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") - - async def validate_repository(self): - """Validate.""" - # Run common validation steps. - await self.common_validate() - - # Custom step 1: Validate content. - if self.data.content_in_root: - self.content.path.remote = "" - - compliant = False - for treefile in self.treefiles: - if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( - ".py" - ): - compliant = True - break - if not compliant: - raise HacsException( - f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" - ) - - # Handle potential errors - if self.validate.errors: - for error in self.validate.errors: - if not self.hacs.system.status.startup: - self.logger.error(error) - return self.validate.success - - async def registration(self): - """Registration.""" - if not await self.validate_repository(): - return False - - # Run common registration steps. - await self.common_registration() - - # Set name - find_file_name(self) - - async def update_repository(self): # lgtm[py/similar-function] - """Update.""" - if self.hacs.github.ratelimits.remaining == 0: - return - # Run common update steps. - await self.common_update() - - # Get python_script objects. - if self.data.content_in_root: - self.content.path.remote = "" - - compliant = False - for treefile in self.treefiles: - if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( - ".py" - ): - compliant = True - break - if not compliant: - raise HacsException( - f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" - ) - - # Update name - find_file_name(self) +"""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.helpers.classes.repository import HacsRepository +from custom_components.hacs.helpers.functions.information import find_file_name + + +class HacsPythonScript(HacsRepository): + """python_scripts in HACS.""" + + category = "python_script" + + def __init__(self, full_name): + """Initialize.""" + super().__init__() + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.PYTHON_SCRIPT + self.content.path.remote = "python_scripts" + self.content.path.local = self.localpath + self.content.single = True + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/python_scripts" + + async def validate_repository(self): + """Validate.""" + # Run common validation steps. + await self.common_validate() + + # Custom step 1: Validate content. + if self.data.content_in_root: + self.content.path.remote = "" + + compliant = False + for treefile in self.treefiles: + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( + ".py" + ): + compliant = True + break + if not compliant: + raise HacsException( + f"Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self, error) + return self.validate.success + + async def async_post_registration(self): + """Registration.""" + # Set name + find_file_name(self) + + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force): + return + + # Get python_script objects. + if self.data.content_in_root: + self.content.path.remote = "" + + compliant = False + for treefile in self.treefiles: + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( + ".py" + ): + compliant = True + break + if not compliant: + raise HacsException( + f"Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + # Update name + find_file_name(self) diff --git a/custom_components/hacs/repositories/repository.py b/custom_components/hacs/repositories/repository.py deleted file mode 100644 index b471683..0000000 --- a/custom_components/hacs/repositories/repository.py +++ /dev/null @@ -1,424 +0,0 @@ -"""Repository.""" -# pylint: disable=broad-except, bad-continuation, no-member -import json -import os -import tempfile -import zipfile -from integrationhelper import Validate -from aiogithubapi import AIOGitHubException -from .manifest import HacsManifest -from ..helpers.misc import get_repository_name -from ..handler.download import async_download_file, async_save_file -from ..helpers.misc import version_left_higher_then_right -from ..helpers.install import install_repository, version_to_install - -from custom_components.hacs.globals import get_hacs -from custom_components.hacs.helpers.information import ( - get_info_md_content, - get_repository, -) -from custom_components.hacs.helpers.validate_repository import ( - common_validate, - common_update_data, -) -from custom_components.hacs.repositories.repositorydata import RepositoryData - - -class RepositoryVersions: - """Versions.""" - - available = None - available_commit = None - installed = None - installed_commit = None - - -class RepositoryStatus: - """Repository status.""" - - hide = False - installed = False - last_updated = None - new = True - selected_tag = None - show_beta = False - track = True - updated_info = False - first_install = True - - -class RepositoryInformation: - """RepositoryInformation.""" - - additional_info = None - authors = [] - category = None - default_branch = None - description = "" - state = None - full_name = None - file_name = None - javascript_type = None - homeassistant_version = None - last_updated = None - uid = None - stars = 0 - info = None - name = None - topics = [] - - -class RepositoryReleases: - """RepositoyReleases.""" - - last_release = None - last_release_object = None - last_release_object_downloads = None - published_tags = [] - objects = [] - releases = False - downloads = None - - -class RepositoryPath: - """RepositoryPath.""" - - local = None - remote = None - - -class RepositoryContent: - """RepositoryContent.""" - - path = None - files = [] - objects = [] - single = False - - -class HacsRepository: - """HacsRepository.""" - - def __init__(self): - """Set up HacsRepository.""" - self.hacs = get_hacs() - self.data = RepositoryData() - self.content = RepositoryContent() - self.content.path = RepositoryPath() - self.information = RepositoryInformation() - self.repository_object = None - self.status = RepositoryStatus() - self.state = None - self.integration_manifest = {} - self.repository_manifest = HacsManifest.from_dict({}) - self.validate = Validate() - self.releases = RepositoryReleases() - self.versions = RepositoryVersions() - self.pending_restart = False - self.tree = [] - self.treefiles = [] - self.ref = None - - @property - def pending_upgrade(self): - """Return pending upgrade.""" - if self.status.installed: - if self.status.selected_tag is not None: - if self.status.selected_tag == self.data.default_branch: - if self.versions.installed_commit != self.versions.available_commit: - return True - return False - if self.display_installed_version != self.display_available_version: - return True - return False - - @property - def config_flow(self): - """Return bool if integration has config_flow.""" - if self.integration_manifest: - if self.data.full_name == "hacs/integration": - return False - return self.integration_manifest.get("config_flow", False) - return False - - @property - def custom(self): - """Return flag if the repository is custom.""" - if self.data.full_name.split("/")[0] in ["custom-components", "custom-cards"]: - return False - if self.data.full_name.lower() in [x.lower() for x in self.hacs.common.default]: - return False - if self.data.full_name == "hacs/integration": - return False - return True - - @property - def can_install(self): - """Return bool if repository can be installed.""" - target = None - if self.information.homeassistant_version is not None: - target = self.information.homeassistant_version - if self.repository_manifest is not None: - if self.data.homeassistant is not None: - target = self.data.homeassistant - - if target is not None: - if self.releases.releases: - if not version_left_higher_then_right( - self.hacs.system.ha_version, target - ): - return False - return True - - @property - def display_name(self): - """Return display name.""" - return get_repository_name(self) - - @property - def display_status(self): - """Return display_status.""" - if self.status.new: - status = "new" - elif self.pending_restart: - status = "pending-restart" - elif self.pending_upgrade: - status = "pending-upgrade" - elif self.status.installed: - status = "installed" - else: - status = "default" - return status - - @property - def display_status_description(self): - """Return display_status_description.""" - description = { - "default": "Not installed.", - "pending-restart": "Restart pending.", - "pending-upgrade": "Upgrade pending.", - "installed": "No action required.", - "new": "This is a newly added repository.", - } - return description[self.display_status] - - @property - def display_installed_version(self): - """Return display_authors""" - if self.versions.installed is not None: - installed = self.versions.installed - else: - if self.versions.installed_commit is not None: - installed = self.versions.installed_commit - else: - installed = "" - return installed - - @property - def display_available_version(self): - """Return display_authors""" - if self.versions.available is not None: - available = self.versions.available - else: - if self.versions.available_commit is not None: - available = self.versions.available_commit - else: - available = "" - return available - - @property - def display_version_or_commit(self): - """Does the repositoriy use releases or commits?""" - if self.releases.releases: - version_or_commit = "version" - else: - version_or_commit = "commit" - return version_or_commit - - @property - def main_action(self): - """Return the main action.""" - actions = { - "new": "INSTALL", - "default": "INSTALL", - "installed": "REINSTALL", - "pending-restart": "REINSTALL", - "pending-upgrade": "UPGRADE", - } - return actions[self.display_status] - - async def common_validate(self): - """Common validation steps of the repository.""" - await common_validate(self) - - async def common_registration(self): - """Common registration steps of the repository.""" - # Attach repository - if self.repository_object is None: - self.repository_object = await get_repository( - self.hacs.session, self.hacs.configuration.token, self.data.full_name - ) - self.data.update_data(self.repository_object.attributes) - - # Set id - self.information.uid = str(self.data.id) - - # Set topics - self.data.topics = self.data.topics - - # Set stargazers_count - self.data.stargazers_count = self.data.stargazers_count - - # Set description - self.data.description = self.data.description - - async def common_update(self): - """Common information update steps of the repository.""" - self.logger.debug("Getting repository information") - - # Attach repository - await common_update_data(self) - - # Update last updaeted - self.information.last_updated = self.repository_object.attributes.get( - "pushed_at", 0 - ) - - # Update last available commit - await self.repository_object.set_last_commit() - self.versions.available_commit = self.repository_object.last_commit - - # Get the content of hacs.json - await self.get_repository_manifest_content() - - # Update "info.md" - self.information.additional_info = await get_info_md_content(self) - - async def install(self): - """Common installation steps of the repository.""" - await install_repository(self) - - async def download_zip(self, validate): - """Download ZIP archive from repository release.""" - try: - contents = False - - for release in self.releases.objects: - self.logger.info(f"ref: {self.ref} --- tag: {release.tag_name}") - if release.tag_name == self.ref.split("/")[1]: - contents = release.assets - - if not contents: - return validate - - for content in contents or []: - filecontent = await async_download_file(content.download_url) - - if filecontent is None: - validate.errors.append(f"[{content.name}] was not downloaded.") - continue - - result = await async_save_file( - f"{tempfile.gettempdir()}/{self.data.filename}", filecontent - ) - with zipfile.ZipFile( - f"{tempfile.gettempdir()}/{self.data.filename}", "r" - ) as zip_file: - zip_file.extractall(self.content.path.local) - - if result: - self.logger.info(f"download of {content.name} complete") - continue - validate.errors.append(f"[{content.name}] was not downloaded.") - except Exception: - validate.errors.append(f"Download was not complete.") - - return validate - - async def download_content(self, validate, directory_path, local_directory, ref): - """Download the content of a directory.""" - from custom_components.hacs.helpers.download import download_content - - validate = await download_content(self) - return validate - - async def get_repository_manifest_content(self): - """Get the content of the hacs.json file.""" - if not "hacs.json" in [x.filename for x in self.tree]: - return - if self.ref is None: - self.ref = version_to_install(self) - try: - manifest = await self.repository_object.get_contents("hacs.json", self.ref) - self.repository_manifest = HacsManifest.from_dict( - json.loads(manifest.content) - ) - self.data.update_data(json.loads(manifest.content)) - except (AIOGitHubException, Exception): # Gotta Catch 'Em All - pass - - def remove(self): - """Run remove tasks.""" - self.logger.info("Starting removal") - - if self.information.uid in self.hacs.common.installed: - self.hacs.common.installed.remove(self.information.uid) - for repository in self.hacs.repositories: - if repository.information.uid == self.information.uid: - self.hacs.repositories.remove(repository) - - async def uninstall(self): - """Run uninstall tasks.""" - self.logger.info("Uninstalling") - await self.remove_local_directory() - self.status.installed = False - if self.data.category == "integration": - if self.config_flow: - await self.reload_custom_components() - else: - self.pending_restart = True - elif self.data.category == "theme": - try: - await self.hacs.hass.services.async_call( - "frontend", "reload_themes", {} - ) - except Exception: # pylint: disable=broad-except - pass - if self.data.full_name in self.hacs.common.installed: - self.hacs.common.installed.remove(self.data.full_name) - self.versions.installed = None - self.versions.installed_commit = None - self.hacs.hass.bus.async_fire( - "hacs/repository", - {"id": 1337, "action": "uninstall", "repository": self.data.full_name}, - ) - - async def remove_local_directory(self): - """Check the local directory.""" - import shutil - from asyncio import sleep - - try: - if self.data.category == "python_script": - local_path = "{}/{}.py".format(self.content.path.local, self.data.name) - elif self.data.category == "theme": - local_path = "{}/{}.yaml".format( - self.content.path.local, self.data.name - ) - else: - local_path = self.content.path.local - - if os.path.exists(local_path): - self.logger.debug(f"Removing {local_path}") - - if self.data.category in ["python_script", "theme"]: - os.remove(local_path) - else: - shutil.rmtree(local_path) - - while os.path.exists(local_path): - await sleep(1) - - except Exception as exception: - self.logger.debug(f"Removing {local_path} failed with {exception}") - return diff --git a/custom_components/hacs/repositories/repositorydata.py b/custom_components/hacs/repositories/repositorydata.py deleted file mode 100644 index 60b6775..0000000 --- a/custom_components/hacs/repositories/repositorydata.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Repository data.""" -from datetime import datetime -from typing import List -import attr - - -@attr.s(auto_attribs=True) -class RepositoryData: - """RepositoryData class.""" - - id: int = 0 - full_name: str = "" - pushed_at: str = "" - category: str = "" - archived: bool = False - description: str = "" - manifest_name: str = None - topics: List[str] = [] - fork: bool = False - domain: str = "" - default_branch: str = None - stargazers_count: int = 0 - last_commit: str = "" - file_name: str = "" - content_in_root: bool = False - zip_release: bool = False - filename: str = "" - render_readme: bool = False - hide_default_branch: bool = False - domains: List[str] = [] - country: List[str] = [] - authors: List[str] = [] - homeassistant: str = None # Minimum Home Assistant version - hacs: str = None # Minimum HACS version - persistent_directory: str = None - iot_class: str = None - - @property - def name(self): - """Return the name.""" - if self.category in ["integration", "netdaemon"]: - return self.domain - return self.full_name.split("/")[-1] - - def to_json(self): - """Export to json.""" - return self.__dict__ - - @staticmethod - def create_from_dict(source: dict): - """Set attributes from dicts.""" - data = RepositoryData() - for key in source: - if key in data.__dict__: - if key == "pushed_at": - setattr( - data, key, datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%SZ") - ) - elif key == "county": - if isinstance(source[key], str): - setattr(data, key, [source[key]]) - else: - setattr(data, key, source[key]) - else: - setattr(data, key, source[key]) - return data - - def update_data(self, data: dict): - """Update data of the repository.""" - for key in data: - if key in self.__dict__: - if key == "pushed_at": - setattr( - self, key, datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%SZ") - ) - elif key == "county": - if isinstance(data[key], str): - setattr(self, key, [data[key]]) - else: - setattr(self, key, data[key]) - else: - setattr(self, key, data[key]) diff --git a/custom_components/hacs/repositories/theme.py b/custom_components/hacs/repositories/theme.py index 6e09657..65234e2 100644 --- a/custom_components/hacs/repositories/theme.py +++ b/custom_components/hacs/repositories/theme.py @@ -1,72 +1,76 @@ -"""Class for themes in HACS.""" -from integrationhelper import Logger -from .repository import HacsRepository -from ..hacsbase.exceptions import HacsException -from ..helpers.information import find_file_name - - -class HacsTheme(HacsRepository): - """Themes in HACS.""" - - def __init__(self, full_name): - """Initialize.""" - super().__init__() - self.data.full_name = full_name - self.data.category = "theme" - self.content.path.remote = "themes" - self.content.path.local = f"{self.hacs.system.config_path}/themes/" - self.content.single = False - self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") - - async def validate_repository(self): - """Validate.""" - # Run common validation steps. - await self.common_validate() - - # Custom step 1: Validate content. - compliant = False - for treefile in self.treefiles: - if treefile.startswith("themes/") and treefile.endswith(".yaml"): - compliant = True - break - if not compliant: - raise HacsException( - f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" - ) - - if self.data.content_in_root: - self.content.path.remote = "" - - # Handle potential errors - if self.validate.errors: - for error in self.validate.errors: - if not self.hacs.system.status.startup: - self.logger.error(error) - return self.validate.success - - async def registration(self): - """Registration.""" - if not await self.validate_repository(): - return False - - # Run common registration steps. - await self.common_registration() - - # Set name - find_file_name(self) - self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" - - async def update_repository(self): # lgtm[py/similar-function] - """Update.""" - if self.hacs.github.ratelimits.remaining == 0: - return - # Run common update steps. - await self.common_update() - - # Get theme objects. - if self.data.content_in_root: - self.content.path.remote = "" - - # Update name - find_file_name(self) - self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" +"""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.helpers.classes.repository import HacsRepository +from custom_components.hacs.helpers.functions.information import find_file_name + + +class HacsTheme(HacsRepository): + """Themes in HACS.""" + + def __init__(self, full_name): + """Initialize.""" + super().__init__() + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.THEME + self.content.path.remote = "themes" + self.content.path.local = self.localpath + self.content.single = False + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" + + async def async_post_installation(self): + """Run post installation steps.""" + try: + await self.hacs.hass.services.async_call("frontend", "reload_themes", {}) + except (Exception, BaseException): # pylint: disable=broad-except + pass + + async def validate_repository(self): + """Validate.""" + # Run common validation steps. + await self.common_validate() + + # Custom step 1: Validate content. + compliant = False + for treefile in self.treefiles: + if treefile.startswith("themes/") and treefile.endswith(".yaml"): + compliant = True + break + if not compliant: + raise HacsException( + f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" + ) + + if self.data.content_in_root: + self.content.path.remote = "" + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self, error) + return self.validate.success + + async def async_post_registration(self): + """Registration.""" + # Set name + find_file_name(self) + self.content.path.local = self.localpath + + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force): + return + + # Get theme objects. + if self.data.content_in_root: + self.content.path.remote = "" + + # Update name + find_file_name(self) + self.content.path.local = self.localpath diff --git a/custom_components/hacs/sensor.py b/custom_components/hacs/sensor.py index cda52de..d5f6cea 100644 --- a/custom_components/hacs/sensor.py +++ b/custom_components/hacs/sensor.py @@ -1,96 +1,123 @@ -"""Sensor platform for HACS.""" -# pylint: disable=unused-argument -from homeassistant.helpers.entity import Entity -from .hacsbase import Hacs as hacs -from .const import DOMAIN, VERSION, NAME_SHORT - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Setup sensor platform.""" - async_add_entities([HACSSensor()]) - - -async def async_setup_entry(hass, config_entry, async_add_devices): - """Setup sensor platform.""" - async_add_devices([HACSSensor()]) - - -class HACSDevice(Entity): - """HACS Device class.""" - - @property - def device_info(self): - """Return device information about HACS.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": NAME_SHORT, - "manufacturer": "hacs.xyz", - "model": "", - "sw_version": VERSION, - } - - -class HACSSensor(HACSDevice): - """HACS Sensor class.""" - - def __init__(self): - """Initialize.""" - self._state = None - self.repositories = [] - - async def async_update(self): - """Update the sensor.""" - if hacs.system.status.background_task: - return - - self.repositories = [] - - for repository in hacs.repositories: - if ( - repository.pending_upgrade - and repository.data.category in hacs.common.categories - ): - self.repositories.append(repository) - self._state = len(self.repositories) - - @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" - ) - - @property - def name(self): - """Return the name of the sensor.""" - return "hacs" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon of the sensor.""" - return "hacs:hacs" - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return "pending update(s)" - - @property - def device_state_attributes(self): - """Return attributes for the sensor.""" - data = [] - for repository in self.repositories: - data.append( - { - "name": repository.data.full_name, - "display_name": repository.display_name, - "installed version": repository.display_installed_version, - "available version": repository.display_available_version, - } - ) - return {"repositories": data} +"""Sensor platform for HACS.""" +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 + + +async def async_setup_platform( + _hass, _config, async_add_entities, _discovery_info=None +): + """Setup sensor platform.""" + async_add_entities([HACSSensor()]) + + +async def async_setup_entry(_hass, _config_entry, async_add_devices): + """Setup sensor platform.""" + async_add_devices([HACSSensor()]) + + +class HACSDevice(Entity): + """HACS Device class.""" + + @property + def device_info(self): + """Return device information about HACS.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": NAME_SHORT, + "manufacturer": "hacs.xyz", + "model": "", + "sw_version": INTEGRATION_VERSION, + "entry_type": "service", + } + + +class HACSSensor(HACSDevice): + """HACS Sensor class.""" + + def __init__(self): + """Initialize.""" + self._state = None + self.repositories = [] + + @property + def should_poll(self): + """No polling needed.""" + return False + + async def async_update(self): + """Manual updates of the sensor.""" + self._update() + + @callback + def _update_and_write_state(self, *_): + """Update the sensor and write state.""" + self._update() + self.async_write_ha_state() + + @callback + def _update(self): + """Update the sensor.""" + hacs = get_hacs() + if hacs.status.background_task: + return + + self.repositories = [] + + for repository in hacs.repositories: + if ( + repository.pending_upgrade + and repository.data.category in hacs.common.categories + ): + self.repositories.append(repository) + self._state = len(self.repositories) + + @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" + ) + + @property + def name(self): + """Return the name of the sensor.""" + return "hacs" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Return the icon of the sensor.""" + return "hacs:hacs" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "pending update(s)" + + @property + def device_state_attributes(self): + """Return attributes for the sensor.""" + repositories = [] + for repository in self.repositories: + repositories.append( + { + "name": repository.data.full_name, + "display_name": repository.display_name, + "installed_version": repository.display_installed_version, + "available_version": repository.display_available_version, + } + ) + return {"repositories": repositories} + + async def async_added_to_hass(self) -> None: + """Register for status events.""" + self.async_on_remove( + self.hass.bus.async_listen("hacs/status", self._update_and_write_state) + ) diff --git a/custom_components/hacs/services.yaml b/custom_components/hacs/services.yaml deleted file mode 100644 index 982a956..0000000 --- a/custom_components/hacs/services.yaml +++ /dev/null @@ -1,15 +0,0 @@ -install: - description: This is NOT intended to be used here, this is intended for developers! - fields: - repository: - description: The repository ID - example: '"123456789"' -register: - description: This is NOT intended to be used here, this is intended for developers! - fields: - repository: - description: The full name of the repository - example: 'developer/repo' - repository_type: - description: The repository type - example: 'plugin' \ No newline at end of file diff --git a/custom_components/hacs/setup.py b/custom_components/hacs/setup.py deleted file mode 100644 index af7f9a0..0000000 --- a/custom_components/hacs/setup.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Setup functions for HACS.""" -# pylint: disable=bad-continuation -from hacs_frontend.version import VERSION as FE_VERSION -from homeassistant.helpers import discovery - -from custom_components.hacs.hacsbase.exceptions import HacsException -from custom_components.hacs.const import VERSION, DOMAIN -from custom_components.hacs.globals import get_hacs -from custom_components.hacs.helpers.information import get_repository -from custom_components.hacs.helpers.register_repository import register_repository - - -async def load_hacs_repository(): - """Load HACS repositroy.""" - hacs = get_hacs() - - 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.status.installed = True - repository.versions.installed = VERSION - repository.status.new = False - hacs.repo = repository.repository_object - hacs.data_repo = await get_repository( - hacs.session, hacs.configuration.token, "hacs/default" - ) - except HacsException as exception: - if "403" in f"{exception}": - hacs.logger.critical("GitHub API is ratelimited, or the token is wrong.") - else: - hacs.logger.critical(f"[{exception}] - Could not load HACS!") - return False - return True - - -def setup_extra_stores(): - """Set up extra stores in HACS if enabled in Home Assistant.""" - hacs = get_hacs() - if "python_script" in hacs.hass.config.components: - if "python_script" not in hacs.common.categories: - hacs.common.categories.append("python_script") - - if hacs.hass.services.services.get("frontend", {}).get("reload_themes") is not None: - if "theme" not in hacs.common.categories: - hacs.common.categories.append("theme") - - -def add_sensor(): - """Add sensor.""" - hacs = get_hacs() - - try: - 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" - ) - ) - except ValueError: - pass - - -async def setup_frontend(): - """Configure the HACS frontend elements.""" - from .http import HacsFrontend, HacsPluginViewLegacy - from .ws_api_handlers import setup_ws_api - - hacs = get_hacs() - - hacs.hass.http.register_view(HacsFrontend()) - hacs.frontend.version_running = FE_VERSION - - # Legacy views, remove with 2.0 - hacs.hass.http.register_view(HacsPluginViewLegacy()) - - # Add to sidepanel - custom_panel_config = { - "name": "hacs-frontend", - "embed_iframe": False, - "trust_external": False, - "js_url": f"/hacsfiles/frontend-{hacs.frontend.version_running}.js", - } - - config = {} - config["_panel_custom"] = custom_panel_config - - hacs.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=config, - require_admin=True, - ) - - if "frontend_extra_module_url" not in hacs.hass.data: - hacs.hass.data["frontend_extra_module_url"] = set() - hacs.hass.data["frontend_extra_module_url"].add("/hacsfiles/iconset.js") - - await setup_ws_api(hacs.hass) diff --git a/custom_components/hacs/share.py b/custom_components/hacs/share.py new file mode 100644 index 0000000..a3665c2 --- /dev/null +++ b/custom_components/hacs/share.py @@ -0,0 +1,68 @@ +"""Shared HACS elements.""" +import os + +from .base import HacsBase + +SHARE = { + "hacs": None, + "factory": None, + "queue": None, + "removed_repositories": [], + "rules": {}, +} + + +def get_hacs() -> HacsBase: + if SHARE["hacs"] is None: + from custom_components.hacs.hacsbase.hacs import Hacs as Legacy + + _hacs = Legacy() + + if not "PYTEST" in os.environ and "GITHUB_ACTION" in os.environ: + _hacs.system.action = True + + SHARE["hacs"] = _hacs + + return SHARE["hacs"] + + +def get_factory(): + if SHARE["factory"] is None: + from custom_components.hacs.operational.factory import HacsTaskFactory + + SHARE["factory"] = HacsTaskFactory() + + return SHARE["factory"] + + +def get_queue(): + if SHARE["queue"] is None: + from queueman import QueueManager + + SHARE["queue"] = QueueManager() + + return SHARE["queue"] + + +def is_removed(repository): + return repository in [x.repository for x in SHARE["removed_repositories"]] + + +def get_removed(repository): + if not is_removed(repository): + from custom_components.hacs.helpers.classes.removed import RemovedRepository + + removed_repo = RemovedRepository() + 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() + ] + + return filter_repos.pop() or None + + +def list_removed_repositories(): + return SHARE["removed_repositories"] diff --git a/custom_components/hacs/store.py b/custom_components/hacs/store.py deleted file mode 100644 index 848d4af..0000000 --- a/custom_components/hacs/store.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Storage handers.""" -from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.storage import Store -from .hacsbase.const import STORAGE_VERSION - - -async def async_load_from_store(hass, key): - """Load the retained data from store and return de-serialized data.""" - store = Store(hass, STORAGE_VERSION, f"hacs.{key}", encoder=JSONEncoder) - restored = await store.async_load() - if restored is None: - return {} - return restored - - -async def async_save_to_store(hass, key, data): - """Generate dynamic data to store and save it to the filesystem.""" - store = Store(hass, STORAGE_VERSION, f"hacs.{key}", encoder=JSONEncoder) - await store.async_save(data) diff --git a/custom_components/hacs/system_health.py b/custom_components/hacs/system_health.py new file mode 100644 index 0000000..37c03d1 --- /dev/null +++ b/custom_components/hacs/system_health.py @@ -0,0 +1,37 @@ +"""Provide info to system health.""" +from aiogithubapi.common.const import BASE_API_URL +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +from .base import HacsBase +from .const import DOMAIN + +GITHUB_STATUS = "https://www.githubstatus.com/" + + +@callback +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") + + +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] + ), + } diff --git a/custom_components/hacs/translations/en.json b/custom_components/hacs/translations/en.json new file mode 100644 index 0000000..12c6c07 --- /dev/null +++ b/custom_components/hacs/translations/en.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of HACS is allowed.", + "min_ha_version": "You need at least version {version} of Home Assistant to setup HACS.", + "github": "Could not authenticate with GitHub, try again later.", + "not_setup": "HACS is not setup." + }, + "error": { + "auth": "Personal Access Token is not correct", + "acc": "You need to acknowledge all the statements before continuing" + }, + "step": { + "user": { + "data": { + "acc_logs": "I know how to access Home Assistant logs", + "acc_addons": "I know that there are no add-ons in HACS", + "acc_untested": "I know that everything inside HACS is custom and untested by Home Assistant", + "acc_disable": "I know that if I get issues with Home Assistant I should disable all my custom_components" + }, + "description": "Before you can setup HACS you need to acknowledge the following", + "title": "HACS" + }, + "device": { + "title": "Waiting for device activation" + } + }, + "progress": { + "wait_for_device": "1. Open {url} \n2.Paste the following key to authorize HACS: \n```\n{code}\n```\n" + } + }, + "options": { + "abort": { + "not_setup": "HACS is not setup.", + "release_limit_value": "The release limit needs to be between 1 and 100" + }, + "step": { + "user": { + "data": { + "not_in_use": "Not in use with YAML", + "country": "Filter with country code.", + "experimental": "Enable experimental features", + "release_limit": "Number of releases to show.", + "debug": "Enable debug.", + "appdaemon": "Enable AppDaemon apps discovery & tracking", + "netdaemon": "Enable NetDaemon apps discovery & tracking", + "sidepanel_icon": "Side panel icon", + "sidepanel_title": "Side panel title" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/hacs/validate/README.md b/custom_components/hacs/validate/README.md new file mode 100644 index 0000000..e0a7fd3 --- /dev/null +++ b/custom_components/hacs/validate/README.md @@ -0,0 +1,38 @@ +# Repository validation + +This is where the validation rules that run against the various repository categories live. + +## Structure + +- All validation rules are in the directory for their category. +- Validation rules that aplies to all categories are in the `common` directory. +- There is one file pr. rule. +- All rule needs tests to verify every possible outcome for the rule. +- It's better with multiple files than a big rule. +- All rules uses `ValidationBase` or `ActionValidationBase` as the base class. +- The `ActionValidationBase` are for checks that will breaks compatibility with with existing repositories (default), so these are only run in github actions. +- The class name should describe what the check does. +- Only use `validate` or `async_validate` methods to define validation rules. +- If a rule should fail, raise `ValidationException` with the failure message. + + +## Example + +```python +from custom_components.hacs.validate.base import ( + ActionValidationBase, + ValidationBase, + ValidationException, +) + + +class AwesomeRepository(ValidationBase): + def validate(self): + if self.repository != "awesome": + raise ValidationException("The repository is not awesome") + +class SuperAwesomeRepository(ActionValidationBase, category="integration"): + async def async_validate(self): + if self.repository != "super-awesome": + raise ValidationException("The repository is not super-awesome") +``` \ No newline at end of file diff --git a/custom_components/hacs/validate/__init__.py b/custom_components/hacs/validate/__init__.py new file mode 100644 index 0000000..42ec85f --- /dev/null +++ b/custom_components/hacs/validate/__init__.py @@ -0,0 +1,51 @@ +import asyncio +import glob +import importlib +from os.path import dirname, join, sep + +from custom_components.hacs.share import SHARE, get_hacs + + +def _initialize_rules(): + rules = glob.glob(join(dirname(__file__), "**/*.py")) + for rule in rules: + rule = rule.replace(sep, "/") + rule = rule.split("custom_components/hacs")[-1] + rule = f"custom_components/hacs{rule}".replace("/", ".")[:-3] + importlib.import_module(rule) + + +async def async_initialize_rules(): + hass = get_hacs().hass + await hass.async_add_executor_job(_initialize_rules) + + +async def async_run_repository_checks(repository): + hacs = get_hacs() + if not SHARE["rules"]: + await async_initialize_rules() + if not hacs.system.running: + return + checks = [] + for check in SHARE["rules"].get("common", []): + checks.append(check(repository)) + for check in SHARE["rules"].get(repository.data.category, []): + checks.append(check(repository)) + + await asyncio.gather( + *[ + check._async_run_check() + for check in checks or [] + if hacs.system.action or not check.action_only + ] + ) + + total = len([x for x in checks if hacs.system.action or not x.action_only]) + failed = len([x for x in checks if x.failed]) + + if failed != 0: + repository.logger.error("%s %s/%s checks failed", repository, failed, total) + if hacs.system.action: + exit(1) + else: + repository.logger.debug("%s All (%s) checks passed", repository, total) diff --git a/custom_components/hacs/validate/base.py b/custom_components/hacs/validate/base.py new file mode 100644 index 0000000..481201c --- /dev/null +++ b/custom_components/hacs/validate/base.py @@ -0,0 +1,48 @@ +from custom_components.hacs.share import SHARE, get_hacs + + +class ValidationException(Exception): + pass + + +class ValidationBase: + def __init__(self, repository) -> None: + self.repository = repository + self.hacs = get_hacs() + self.failed = False + self.logger = repository.logger + + def __init_subclass__(cls, category="common", **kwargs) -> None: + """Initialize a subclass, register if possible.""" + super().__init_subclass__(**kwargs) + if SHARE["rules"].get(category) is None: + SHARE["rules"][category] = [] + if cls not in SHARE["rules"][category]: + SHARE["rules"][category].append(cls) + + @property + def action_only(self): + return False + + async def _async_run_check(self): + """DO NOT OVERRIDE THIS IN SUBCLASSES!""" + if self.hacs.system.action: + self.logger.info(f"Running check '{self.__class__.__name__}'") + try: + await self.hacs.hass.async_add_executor_job(self.check) + await self.async_check() + except ValidationException as exception: + self.failed = True + self.logger.error(exception) + + def check(self): + pass + + async def async_check(self): + pass + + +class ActionValidationBase(ValidationBase): + @property + def action_only(self): + return True diff --git a/custom_components/hacs/validate/common/hacs_manifest.py b/custom_components/hacs/validate/common/hacs_manifest.py new file mode 100644 index 0000000..6701c12 --- /dev/null +++ b/custom_components/hacs/validate/common/hacs_manifest.py @@ -0,0 +1,10 @@ +from custom_components.hacs.validate.base import ( + ActionValidationBase, + ValidationException, +) + + +class HacsManifest(ActionValidationBase): + def check(self): + if "hacs.json" not in [x.filename for x in self.repository.tree]: + raise ValidationException("The repository has no 'hacs.json' file") diff --git a/custom_components/hacs/validate/common/repository_description.py b/custom_components/hacs/validate/common/repository_description.py new file mode 100644 index 0000000..100de02 --- /dev/null +++ b/custom_components/hacs/validate/common/repository_description.py @@ -0,0 +1,10 @@ +from custom_components.hacs.validate.base import ( + ActionValidationBase, + ValidationException, +) + + +class RepositoryDescription(ActionValidationBase): + def check(self): + if not self.repository.data.description: + raise ValidationException("The repository has no description") diff --git a/custom_components/hacs/validate/common/repository_information_file.py b/custom_components/hacs/validate/common/repository_information_file.py new file mode 100644 index 0000000..0447988 --- /dev/null +++ b/custom_components/hacs/validate/common/repository_information_file.py @@ -0,0 +1,19 @@ +from custom_components.hacs.validate.base import ( + ActionValidationBase, + ValidationException, +) + + +class RepositoryInformationFile(ActionValidationBase): + async def async_check(self): + filenames = [x.filename.lower() for x in self.repository.tree] + if self.repository.data.render_readme and "readme" in filenames: + pass + elif self.repository.data.render_readme and "readme.md" in filenames: + pass + elif "info" in filenames: + pass + elif "info.md" in filenames: + pass + else: + raise ValidationException("The repository has no information file") diff --git a/custom_components/hacs/validate/common/repository_topics.py b/custom_components/hacs/validate/common/repository_topics.py new file mode 100644 index 0000000..194faad --- /dev/null +++ b/custom_components/hacs/validate/common/repository_topics.py @@ -0,0 +1,10 @@ +from custom_components.hacs.validate.base import ( + ActionValidationBase, + ValidationException, +) + + +class RepositoryTopics(ActionValidationBase): + def check(self): + if not self.repository.data.topics: + raise ValidationException("The repository has no topics") diff --git a/custom_components/hacs/validate/integration/integration_manifest.py b/custom_components/hacs/validate/integration/integration_manifest.py new file mode 100644 index 0000000..41b1c97 --- /dev/null +++ b/custom_components/hacs/validate/integration/integration_manifest.py @@ -0,0 +1,10 @@ +from custom_components.hacs.validate.base import ( + ActionValidationBase, + ValidationException, +) + + +class IntegrationManifest(ActionValidationBase, category="integration"): + def check(self): + if "manifest.json" not in [x.filename for x in self.repository.tree]: + raise ValidationException("The repository has no 'hacs.json' file") diff --git a/custom_components/hacs/webresponses/__init__.py b/custom_components/hacs/webresponses/__init__.py new file mode 100644 index 0000000..4b9a8ba --- /dev/null +++ b/custom_components/hacs/webresponses/__init__.py @@ -0,0 +1 @@ +"""Initialize HACS Web responses""" diff --git a/custom_components/hacs/webresponses/frontend.py b/custom_components/hacs/webresponses/frontend.py new file mode 100644 index 0000000..b1a32f9 --- /dev/null +++ b/custom_components/hacs/webresponses/frontend.py @@ -0,0 +1,26 @@ +from aiohttp import web + +from homeassistant.components.http import HomeAssistantView +from custom_components.hacs.share import get_hacs + + +class HacsFrontendDev(HomeAssistantView): + """Dev View Class for HACS.""" + + requires_auth = False + name = "hacs_files:frontend" + url = r"/hacsfiles/frontend/{requested_file:.+}" + + 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}" + ) + if request.status == 200: + result = await request.read() + response = web.Response(body=result) + response.headers["Content-Type"] = "application/javascript" + + return response diff --git a/custom_components/hacs/ws_api_handlers.py b/custom_components/hacs/ws_api_handlers.py deleted file mode 100644 index 6f52660..0000000 --- a/custom_components/hacs/ws_api_handlers.py +++ /dev/null @@ -1,379 +0,0 @@ -"""WebSocket API for HACS.""" -# pylint: disable=unused-argument -import sys -import os -import voluptuous as vol -from aiogithubapi import AIOGitHubException -from homeassistant.components import websocket_api -import homeassistant.helpers.config_validation as cv -from .hacsbase.exceptions import HacsException -from .store import async_load_from_store, async_save_to_store - -from custom_components.hacs.globals import get_hacs -from custom_components.hacs.helpers.register_repository import register_repository - - -async def setup_ws_api(hass): - """Set up WS API handlers.""" - websocket_api.async_register_command(hass, hacs_settings) - websocket_api.async_register_command(hass, hacs_config) - websocket_api.async_register_command(hass, hacs_repositories) - websocket_api.async_register_command(hass, hacs_repository) - websocket_api.async_register_command(hass, hacs_repository_data) - websocket_api.async_register_command(hass, check_local_path) - websocket_api.async_register_command(hass, hacs_status) - websocket_api.async_register_command(hass, acknowledge_critical_repository) - websocket_api.async_register_command(hass, get_critical_repositories) - - -@websocket_api.async_response -@websocket_api.websocket_command( - { - vol.Required("type"): "hacs/settings", - vol.Optional("action"): cv.string, - vol.Optional("category"): cv.string, - } -) -async def hacs_settings(hass, connection, msg): - """Handle get media player cover command.""" - hacs = get_hacs() - action = msg["action"] - hacs.logger.debug(f"WS action '{action}'") - - if action == "set_fe_grid": - hacs.configuration.frontend_mode = "Grid" - - elif action == "onboarding_done": - hacs.configuration.onboarding_done = True - - elif action == "set_fe_table": - hacs.configuration.frontend_mode = "Table" - - elif action == "set_fe_compact_true": - hacs.configuration.frontend_compact = False - - elif action == "set_fe_compact_false": - hacs.configuration.frontend_compact = True - - elif action == "reload_data": - hacs.system.status.reloading_data = True - hass.bus.async_fire("hacs/status", {}) - await hacs.recuring_tasks_all() - hacs.system.status.reloading_data = False - hass.bus.async_fire("hacs/status", {}) - - elif action == "upgrade_all": - hacs.system.status.upgrading_all = True - hacs.system.status.background_task = True - hass.bus.async_fire("hacs/status", {}) - for repository in hacs.repositories: - if repository.pending_upgrade: - repository.status.selected_tag = None - await repository.install() - hacs.system.status.upgrading_all = False - hacs.system.status.background_task = False - hass.bus.async_fire("hacs/status", {}) - hass.bus.async_fire("hacs/repository", {}) - - elif action == "clear_new": - for repo in hacs.repositories: - if msg.get("category") == repo.data.category: - if repo.status.new: - hacs.logger.debug( - f"Clearing new flag from '{repo.data.full_name}'" - ) - repo.status.new = False - else: - hacs.logger.error(f"WS action '{action}' is not valid") - hass.bus.async_fire("hacs/config", {}) - await hacs.data.async_write() - - -@websocket_api.async_response -@websocket_api.websocket_command({vol.Required("type"): "hacs/config"}) -async def hacs_config(hass, connection, msg): - """Handle get media player cover command.""" - hacs = get_hacs() - config = hacs.configuration - - content = {} - content["frontend_mode"] = config.frontend_mode - content["frontend_compact"] = config.frontend_compact - content["onboarding_done"] = config.onboarding_done - content["version"] = hacs.version - content["dev"] = config.dev - content["debug"] = config.debug - content["country"] = config.country - content["experimental"] = config.experimental - content["categories"] = hacs.common.categories - - connection.send_message(websocket_api.result_message(msg["id"], content)) - - -@websocket_api.async_response -@websocket_api.websocket_command({vol.Required("type"): "hacs/status"}) -async def hacs_status(hass, connection, msg): - """Handle get media player cover command.""" - hacs = get_hacs() - content = { - "startup": hacs.system.status.startup, - "background_task": hacs.system.status.background_task, - "lovelace_mode": hacs.system.lovelace_mode, - "reloading_data": hacs.system.status.reloading_data, - "upgrading_all": hacs.system.status.upgrading_all, - "disabled": hacs.system.disabled, - } - connection.send_message(websocket_api.result_message(msg["id"], content)) - - -@websocket_api.async_response -@websocket_api.websocket_command({vol.Required("type"): "hacs/repositories"}) -async def hacs_repositories(hass, connection, msg): - """Handle get media player cover command.""" - hacs = get_hacs() - repositories = hacs.repositories - content = [] - for repo in repositories: - if repo.data.category in hacs.common.categories: - data = { - "additional_info": repo.information.additional_info, - "authors": repo.data.authors, - "available_version": repo.display_available_version, - "beta": repo.status.show_beta, - "can_install": repo.can_install, - "category": repo.data.category, - "country": repo.data.country, - "config_flow": repo.config_flow, - "custom": repo.custom, - "default_branch": repo.data.default_branch, - "description": repo.data.description, - "domain": repo.integration_manifest.get("domain"), - "downloads": repo.releases.downloads, - "file_name": repo.data.file_name, - "first_install": repo.status.first_install, - "full_name": repo.data.full_name, - "hide": repo.status.hide, - "hide_default_branch": repo.data.hide_default_branch, - "homeassistant": repo.data.homeassistant, - "id": repo.information.uid, - "info": repo.information.info, - "installed_version": repo.display_installed_version, - "installed": repo.status.installed, - "javascript_type": repo.information.javascript_type, - "last_updated": repo.information.last_updated, - "local_path": repo.content.path.local, - "main_action": repo.main_action, - "name": repo.display_name, - "new": repo.status.new, - "pending_upgrade": repo.pending_upgrade, - "releases": repo.releases.published_tags, - "selected_tag": repo.status.selected_tag, - "stars": repo.data.stargazers_count, - "state": repo.state, - "status_description": repo.display_status_description, - "status": repo.display_status, - "topics": repo.data.topics, - "updated_info": repo.status.updated_info, - "version_or_commit": repo.display_version_or_commit, - } - - content.append(data) - - connection.send_message(websocket_api.result_message(msg["id"], content)) - - -@websocket_api.async_response -@websocket_api.websocket_command( - { - vol.Required("type"): "hacs/repository", - vol.Optional("action"): cv.string, - vol.Optional("repository"): cv.string, - } -) -async def hacs_repository(hass, connection, msg): - """Handle get media player cover command.""" - hacs = get_hacs() - try: - repo_id = msg.get("repository") - action = msg.get("action") - - if repo_id is None or action is None: - return - - repository = hacs.get_by_id(repo_id) - hacs.logger.debug(f"Running {action} for {repository.data.full_name}") - - if action == "update": - await repository.update_repository() - repository.status.updated_info = True - repository.status.new = False - - elif action == "install": - was_installed = repository.status.installed - await repository.install() - if not was_installed: - hass.bus.async_fire("hacs/reload", {"force": False}) - - elif action == "uninstall": - await repository.uninstall() - hass.bus.async_fire("hacs/reload", {"force": False}) - - elif action == "hide": - repository.status.hide = True - - elif action == "unhide": - repository.status.hide = False - - elif action == "show_beta": - repository.status.show_beta = True - await repository.update_repository() - - elif action == "hide_beta": - repository.status.show_beta = False - await repository.update_repository() - - elif action == "delete": - repository.status.show_beta = False - repository.remove() - - elif action == "set_version": - if msg["version"] == repository.data.default_branch: - repository.status.selected_tag = None - else: - repository.status.selected_tag = msg["version"] - await repository.update_repository() - - else: - hacs.logger.error(f"WS action '{action}' is not valid") - - repository.state = None - await hacs.data.async_write() - except AIOGitHubException as exception: - hass.bus.async_fire("hacs/error", {"message": str(exception)}) - except AttributeError as exception: - hass.bus.async_fire( - "hacs/error", {"message": f"Could not use repository with ID {repo_id}"} - ) - except Exception as exception: # pylint: disable=broad-except - hass.bus.async_fire("hacs/error", {"message": str(exception)}) - - -@websocket_api.async_response -@websocket_api.websocket_command( - { - vol.Required("type"): "hacs/repository/data", - vol.Optional("action"): cv.string, - vol.Optional("repository"): cv.string, - vol.Optional("data"): cv.string, - } -) -async def hacs_repository_data(hass, connection, msg): - """Handle get media player cover command.""" - hacs = get_hacs() - repo_id = msg.get("repository") - action = msg.get("action") - data = msg.get("data") - - if repo_id is None: - return - - if action == "add": - if "github." in repo_id: - repo_id = repo_id.split("github.com/")[1] - - if repo_id in hacs.common.skip: - hacs.common.skip.remove(repo_id) - - if not hacs.get_by_name(repo_id): - try: - registration = await register_repository(repo_id, data.lower()) - if registration is not None: - raise HacsException(registration) - except Exception as exception: # pylint: disable=broad-except - hass.bus.async_fire( - "hacs/error", - { - "action": "add_repository", - "exception": str(sys.exc_info()[0].__name__), - "message": str(exception), - }, - ) - else: - hass.bus.async_fire( - "hacs/error", - { - "action": "add_repository", - "message": f"Repository '{repo_id}' exists in the store.", - }, - ) - - repository = hacs.get_by_name(repo_id) - else: - repository = hacs.get_by_id(repo_id) - - if repository is None: - hass.bus.async_fire("hacs/repository", {}) - return - - hacs.logger.debug(f"Running {action} for {repository.data.full_name}") - - if action == "set_state": - repository.state = data - - elif action == "set_version": - repository.status.selected_tag = data - await repository.update_repository() - repository.state = None - - elif action == "add": - repository.state = None - - else: - repository.state = None - hacs.logger.error(f"WS action '{action}' is not valid") - - await hacs.data.async_write() - - -@websocket_api.async_response -@websocket_api.websocket_command( - {vol.Required("type"): "hacs/check_path", vol.Optional("path"): cv.string} -) -async def check_local_path(hass, connection, msg): - """Handle get media player cover command.""" - path = msg.get("path") - exist = {"exist": False} - - if path is None: - return - - if os.path.exists(path): - exist["exist"] = True - - connection.send_message(websocket_api.result_message(msg["id"], exist)) - - -@websocket_api.async_response -@websocket_api.websocket_command({vol.Required("type"): "hacs/get_critical"}) -async def get_critical_repositories(hass, connection, msg): - """Handle get media player cover command.""" - critical = await async_load_from_store(hass, "critical") - if not critical: - critical = [] - connection.send_message(websocket_api.result_message(msg["id"], critical)) - - -@websocket_api.async_response -@websocket_api.websocket_command( - {vol.Required("type"): "hacs/critical", vol.Optional("repository"): cv.string} -) -async def acknowledge_critical_repository(hass, connection, msg): - """Handle get media player cover command.""" - repository = msg["repository"] - - critical = await async_load_from_store(hass, "critical") - for repo in critical: - if repository == repo["repository"]: - repo["acknowledged"] = True - await async_save_to_store(hass, "critical", critical) - connection.send_message(websocket_api.result_message(msg["id"], critical)) diff --git a/custom_components/nodered/__init__.py b/custom_components/nodered/__init__.py new file mode 100644 index 0000000..5da8553 --- /dev/null +++ b/custom_components/nodered/__init__.py @@ -0,0 +1,239 @@ +""" +Component to integrate with node-red. + +For more details about this component, please refer to +https://github.com/zachowj/hass-node-red +""" +import asyncio +import logging +from typing import Any, Dict, Optional, Union + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_STATE, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity + +from .const import ( + CONF_COMPONENT, + CONF_CONFIG, + CONF_DEVICE_INFO, + CONF_NAME, + CONF_NODE_ID, + CONF_REMOVE, + CONF_SERVER_ID, + CONF_VERSION, + DOMAIN, + DOMAIN_DATA, + NAME, + NODERED_DISCOVERY_UPDATED, + NODERED_ENTITY, + STARTUP_MESSAGE, + VERSION, +) +from .discovery import ( + ALREADY_DISCOVERED, + CHANGE_ENTITY_TYPE, + CONFIG_ENTRY_IS_SETUP, + NODERED_DISCOVERY, + start_discovery, + stop_discovery, +) +from .websocket import register_websocket_handlers + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up this integration using YAML is not supported.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up this integration using UI.""" + + if hass.data.get(DOMAIN_DATA) is None: + hass.data.setdefault(DOMAIN_DATA, {}) + _LOGGER.info(STARTUP_MESSAGE) + + register_websocket_handlers(hass) + await start_discovery(hass, hass.data[DOMAIN_DATA], entry) + hass.bus.async_fire(DOMAIN, {CONF_TYPE: "loaded", CONF_VERSION: VERSION}) + + entry.add_update_listener(async_reload_entry) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Handle removal of an entry.""" + unloaded = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in hass.data[DOMAIN_DATA][CONFIG_ENTRY_IS_SETUP] + ] + ) + ) + + if unloaded: + stop_discovery(hass) + hass.data.pop(DOMAIN_DATA) + hass.bus.async_fire(DOMAIN, {CONF_TYPE: "unloaded"}) + + return unloaded + + +class NodeRedEntity(Entity): + """nodered Sensor class.""" + + def __init__(self, hass, config): + """Initialize the entity.""" + self.hass = hass + self.attr = {} + self._config = config[CONF_CONFIG] + self._component = None + self._device_info = config.get(CONF_DEVICE_INFO) + self._state = None + self._server_id = config[CONF_SERVER_ID] + self._node_id = config[CONF_NODE_ID] + self._remove_signal_discovery_update = None + self._remove_signal_entity_update = None + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID to use for this sensor.""" + return f"{DOMAIN}-{self._server_id}-{self._node_id}" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this binary_sensor.""" + return self._config.get(CONF_DEVICE_CLASS) + + @property + def name(self) -> Optional[str]: + """Return the name of the sensor.""" + return self._config.get(CONF_NAME, f"{NAME} {self._node_id}") + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self._state + + @property + def icon(self) -> Optional[str]: + """Return the icon of the sensor.""" + return self._config.get(CONF_ICON) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit this state is expressed in.""" + return self._config.get(CONF_UNIT_OF_MEASUREMENT) + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes.""" + return self.attr + + @property + def device_info(self) -> Optional[Dict[str, Any]]: + """Return device specific attributes.""" + info = None + if self._device_info is not None and "id" in self._device_info: + # Use the id property to create the device identifier then delete it + info = {"identifiers": {(DOMAIN, self._device_info["id"])}} + del self._device_info["id"] + info.update(self._device_info) + + return info + + @callback + def handle_entity_update(self, msg): + """Update entity state.""" + _LOGGER.debug(f"Entity Update: {msg}") + self.attr = msg.get("attributes", {}) + self._state = msg[CONF_STATE] + self.async_write_ha_state() + + @callback + def handle_discovery_update(self, msg, connection): + """Update entity config.""" + if CONF_REMOVE not in msg: + self._config = msg[CONF_CONFIG] + self.async_write_ha_state() + return + + # Otherwise, remove entity + if msg[CONF_REMOVE] == CHANGE_ENTITY_TYPE: + # recreate entity if component type changed + @callback + def recreate_entity(): + """Create entity with new type.""" + del msg[CONF_REMOVE] + async_dispatcher_send( + self.hass, + NODERED_DISCOVERY.format(msg[CONF_COMPONENT]), + msg, + connection, + ) + + self.async_on_remove(recreate_entity) + + self.hass.async_create_task(self.async_remove()) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + + self._remove_signal_entity_update = async_dispatcher_connect( + self.hass, + NODERED_ENTITY.format(self._server_id, self._node_id), + self.handle_entity_update, + ) + self._remove_signal_discovery_update = async_dispatcher_connect( + self.hass, + NODERED_DISCOVERY_UPDATED.format(self.unique_id), + self.handle_discovery_update, + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + if self._remove_signal_entity_update is not None: + self._remove_signal_entity_update() + if self._remove_signal_discovery_update is not None: + self._remove_signal_discovery_update() + + del self.hass.data[DOMAIN_DATA][ALREADY_DISCOVERED][self.unique_id] + + # Remove the entity_id from the entity registry + registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_id = registry.async_get_entity_id( + self._component, + DOMAIN, + self.unique_id, + ) + if entity_id: + registry.async_remove(entity_id) + _LOGGER.info(f"Entity removed: {entity_id}") + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) diff --git a/custom_components/nodered/binary_sensor.py b/custom_components/nodered/binary_sensor.py new file mode 100644 index 0000000..0bc0dbf --- /dev/null +++ b/custom_components/nodered/binary_sensor.py @@ -0,0 +1,76 @@ +"""Binary sensor platform for nodered.""" +from numbers import Number + +from homeassistant.const import ( + CONF_STATE, + STATE_HOME, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_UNLOCKED, +) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import NodeRedEntity +from .const import CONF_ATTRIBUTES, CONF_BINARY_SENSOR, NODERED_DISCOVERY_NEW + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up sensor platform.""" + + async def async_discover(config, connection): + await _async_setup_entity(hass, config, async_add_devices) + + async_dispatcher_connect( + hass, + NODERED_DISCOVERY_NEW.format(CONF_BINARY_SENSOR), + async_discover, + ) + + +async def _async_setup_entity(hass, config, async_add_devices): + """Set up the Node-RED binary-sensor.""" + async_add_devices([NodeRedBinarySensor(hass, config)]) + + +class NodeRedBinarySensor(NodeRedEntity): + """Node-RED binary-sensor class.""" + + on_states = ( + "1", + "true", + "yes", + "enable", + STATE_ON, + STATE_OPEN, + STATE_HOME, + STATE_UNLOCKED, + ) + + def __init__(self, hass, config): + """Initialize the binary sensor.""" + super().__init__(hass, config) + self._component = CONF_BINARY_SENSOR + self._state = config.get(CONF_STATE) + self.attr = config.get(CONF_ATTRIBUTES, {}) + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + value = self._state + + if isinstance(value, bool): + return value + if isinstance(value, str): + value = value.lower().strip() + if value in NodeRedBinarySensor.on_states: + return True + elif isinstance(value, Number): + return value != 0 + + return False + + @property + def state(self): + """Return the state of the binary sensor.""" + return STATE_ON if self.is_on else STATE_OFF diff --git a/custom_components/nodered/config_flow.py b/custom_components/nodered/config_flow.py new file mode 100644 index 0000000..eb65aa2 --- /dev/null +++ b/custom_components/nodered/config_flow.py @@ -0,0 +1,34 @@ +"""Adds config flow for Node-RED.""" +import logging + +from homeassistant import config_entries + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register(DOMAIN) +class NodeRedFlowHandler(config_entries.ConfigFlow): + """Config flow for Node-RED.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize.""" + self._errors = {} + + async def async_step_user(self, user_input=None): + """Handle a user initiated set up flow to create a webhook.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if self.hass.data.get(DOMAIN): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self.async_show_form(step_id="user") + return self.async_create_entry( + title="Node-RED", + data={}, + ) diff --git a/custom_components/nodered/const.py b/custom_components/nodered/const.py new file mode 100644 index 0000000..56a8870 --- /dev/null +++ b/custom_components/nodered/const.py @@ -0,0 +1,51 @@ +"""Constants for Node-RED.""" +# Base component constants +DOMAIN = "nodered" +DOMAIN_DATA = f"{DOMAIN}_data" +VERSION = "0.5.2" + +ISSUE_URL = "https://github.com/zachowj/hass-node-red/issues" + +# Configuration +CONF_ATTRIBUTES = "attributes" +CONF_BINARY_SENSOR = "binary_sensor" +CONF_COMPONENT = "component" +CONF_CONFIG = "config" +CONF_CONNECTION = "connection" +CONF_DATA = "data" +CONF_DEVICE_INFO = "device_info" +CONF_DEVICE_TRIGGER = "device_trigger" +CONF_ENABLED = "enabled" +CONF_NAME = "name" +CONF_NODE_ID = "node_id" +CONF_OUTPUT_PATH = "output_path" +CONF_PAYLOAD = "payload" +CONF_REMOVE = "remove" +CONF_SENSOR = "sensor" +CONF_SERVER_ID = "server_id" +CONF_SKIP_CONDITION = "skip_condition" +CONF_SUB_TYPE = "sub_type" +CONF_SWITCH = "switch" +CONF_TRIGGER_ENTITY_ID = "trigger_entity_id" +CONF_VERSION = "version" + +NODERED_DISCOVERY = "nodered_discovery" +NODERED_DISCOVERY_NEW = "nodered_discovery_new_{}" +NODERED_DISCOVERY_UPDATED = "nodered_discovery_updated_{}" +NODERED_ENTITY = "nodered_entity_{}_{}" + +SERVICE_TRIGGER = "trigger" + +# Defaults +NAME = DOMAIN +SWITCH_ICON = "mdi:electric-switch-closed" + +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} +------------------------------------------------------------------- +""" diff --git a/custom_components/nodered/discovery.py b/custom_components/nodered/discovery.py new file mode 100644 index 0000000..898108b --- /dev/null +++ b/custom_components/nodered/discovery.py @@ -0,0 +1,109 @@ +"""Support for Node-RED discovery.""" +import asyncio +import logging + +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + CONF_BINARY_SENSOR, + CONF_COMPONENT, + CONF_NODE_ID, + CONF_REMOVE, + CONF_SENSOR, + CONF_SERVER_ID, + CONF_SWITCH, + DOMAIN, + DOMAIN_DATA, + NODERED_DISCOVERY, + NODERED_DISCOVERY_NEW, + NODERED_DISCOVERY_UPDATED, +) + +SUPPORTED_COMPONENTS = [ + CONF_SWITCH, + CONF_BINARY_SENSOR, + CONF_SENSOR, +] + +_LOGGER = logging.getLogger(__name__) + +ALREADY_DISCOVERED = "discovered_components" +CHANGE_ENTITY_TYPE = "change_entity_type" +CONFIG_ENTRY_LOCK = "config_entry_lock" +CONFIG_ENTRY_IS_SETUP = "config_entry_is_setup" +DISCOVERY_DISPATCHED = "discovery_dispatched" + + +async def start_discovery( + hass: HomeAssistantType, hass_config, config_entry=None +) -> bool: + """Initialize of Node-RED Discovery.""" + + async def async_device_message_received(msg, connection): + """Process the received message.""" + component = msg[CONF_COMPONENT] + server_id = msg[CONF_SERVER_ID] + node_id = msg[CONF_NODE_ID] + + if component not in SUPPORTED_COMPONENTS: + _LOGGER.warning(f"Integration {component} is not supported") + return + + discovery_hash = f"{DOMAIN}-{server_id}-{node_id}" + data = hass.data[DOMAIN_DATA] + + _LOGGER.debug(f"Discovery message: {msg}") + + if ALREADY_DISCOVERED not in data: + data[ALREADY_DISCOVERED] = {} + if discovery_hash in data[ALREADY_DISCOVERED]: + + if data[ALREADY_DISCOVERED][discovery_hash] != component: + # Remove old + log_text = f"Changing {data[ALREADY_DISCOVERED][discovery_hash]} to" + msg[CONF_REMOVE] = CHANGE_ENTITY_TYPE + elif CONF_REMOVE in msg: + log_text = "Removing" + else: + # Dispatch update + log_text = "Updating" + + _LOGGER.info(f"{log_text} {component} {server_id} {node_id}") + + data[ALREADY_DISCOVERED][discovery_hash] = component + async_dispatcher_send( + hass, NODERED_DISCOVERY_UPDATED.format(discovery_hash), msg, connection + ) + else: + # Add component + _LOGGER.info(f"Creating {component} {server_id} {node_id}") + data[ALREADY_DISCOVERED][discovery_hash] = component + + async with data[CONFIG_ENTRY_LOCK]: + if component not in data[CONFIG_ENTRY_IS_SETUP]: + await hass.config_entries.async_forward_entry_setup( + config_entry, component + ) + data[CONFIG_ENTRY_IS_SETUP].add(component) + + async_dispatcher_send( + hass, NODERED_DISCOVERY_NEW.format(component), msg, connection + ) + + hass.data[DOMAIN_DATA][CONFIG_ENTRY_LOCK] = asyncio.Lock() + hass.data[DOMAIN_DATA][CONFIG_ENTRY_IS_SETUP] = set() + + hass.data[DOMAIN_DATA][DISCOVERY_DISPATCHED] = async_dispatcher_connect( + hass, + NODERED_DISCOVERY, + async_device_message_received, + ) + + +def stop_discovery(hass: HomeAssistantType): + """Remove discovery dispatcher.""" + hass.data[DOMAIN_DATA][DISCOVERY_DISPATCHED]() diff --git a/custom_components/nodered/manifest.json b/custom_components/nodered/manifest.json new file mode 100644 index 0000000..6915648 --- /dev/null +++ b/custom_components/nodered/manifest.json @@ -0,0 +1,13 @@ +{ + "codeowners": [ + "@zachowj" + ], + "config_flow": true, + "dependencies": [], + "documentation": "https://zachowj.github.io/node-red-contrib-home-assistant-websocket/guide/custom_integration/", + "domain": "nodered", + "iot_class": "local_push", + "issue_tracker": "https://github.com/zachowj/hass-node-red/issues", + "name": "Node-RED Companion", + "version": "0.5.2" +} diff --git a/custom_components/nodered/sensor.py b/custom_components/nodered/sensor.py new file mode 100644 index 0000000..82d6c55 --- /dev/null +++ b/custom_components/nodered/sensor.py @@ -0,0 +1,39 @@ +"""Sensor platform for nodered.""" +import logging + +from homeassistant.const import CONF_STATE +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import NodeRedEntity +from .const import CONF_ATTRIBUTES, CONF_SENSOR, NODERED_DISCOVERY_NEW + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensor platform.""" + + async def async_discover(config, connection): + await _async_setup_entity(hass, config, async_add_entities) + + async_dispatcher_connect( + hass, + NODERED_DISCOVERY_NEW.format(CONF_SENSOR), + async_discover, + ) + + +async def _async_setup_entity(hass, config, async_add_entities): + """Set up the Node-RED sensor.""" + async_add_entities([NodeRedSensor(hass, config)]) + + +class NodeRedSensor(NodeRedEntity): + """Node-RED Sensor class.""" + + def __init__(self, hass, config): + """Initialize the sensor.""" + super().__init__(hass, config) + self._component = CONF_SENSOR + self._state = config.get(CONF_STATE) + self.attr = config.get(CONF_ATTRIBUTES, {}) diff --git a/custom_components/nodered/services.yaml b/custom_components/nodered/services.yaml new file mode 100644 index 0000000..345eba7 --- /dev/null +++ b/custom_components/nodered/services.yaml @@ -0,0 +1,17 @@ +trigger: + description: Trigger a Node-RED Node + fields: + entity_id: + description: Entity Id of the Node-RED switch + example: switch.nodered_motion + trigger_entity_id: + description: Entity Id to trigger the event node with. Only needed if the node is not triggered by a single entity. + example: sun.sun + skip_condition: + description: Skip conditions of the node (defaults to false) + example: true + output_path: + description: Which output of the node to use (defaults to true, the top output). Only used when skip_condition is set to true. + example: true + payload: + description: The payload the node will output when triggered. Work only when triggering a entity node not an event node. diff --git a/custom_components/nodered/switch.py b/custom_components/nodered/switch.py new file mode 100644 index 0000000..21dcc5a --- /dev/null +++ b/custom_components/nodered/switch.py @@ -0,0 +1,247 @@ +"""Sensor platform for nodered.""" +import json +import logging + +import voluptuous as vol + +from homeassistant.components.websocket_api import event_message +from homeassistant.const import ( + CONF_ENTITY_ID, + CONF_ICON, + CONF_ID, + CONF_STATE, + CONF_TYPE, + EVENT_STATE_CHANGED, +) +from homeassistant.core import callback +from homeassistant.helpers import entity_platform, trigger +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import ToggleEntity + +from . import NodeRedEntity +from .const import ( + CONF_CONFIG, + CONF_DATA, + CONF_DEVICE_TRIGGER, + CONF_OUTPUT_PATH, + CONF_PAYLOAD, + CONF_REMOVE, + CONF_SKIP_CONDITION, + CONF_SUB_TYPE, + CONF_SWITCH, + CONF_TRIGGER_ENTITY_ID, + DOMAIN, + NODERED_DISCOVERY_NEW, + SERVICE_TRIGGER, + SWITCH_ICON, +) +from .utils import NodeRedJSONEncoder + +_LOGGER = logging.getLogger(__name__) + +SERVICE_TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_TRIGGER_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_SKIP_CONDITION): cv.boolean, + vol.Optional(CONF_OUTPUT_PATH): cv.boolean, + vol.Optional(CONF_PAYLOAD): vol.Extra, + } +) +EVENT_TRIGGER_NODE = "automation_triggered" +EVENT_DEVICE_TRIGGER = "device_trigger" + +TYPE_SWITCH = "switch" +TYPE_DEVICE_TRIGGER = "device_trigger" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Switch platform.""" + + async def async_discover(config, connection): + await _async_setup_entity(hass, config, async_add_entities, connection) + + async_dispatcher_connect( + hass, + NODERED_DISCOVERY_NEW.format(CONF_SWITCH), + async_discover, + ) + + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_TRIGGER, SERVICE_TRIGGER_SCHEMA, "async_trigger_node" + ) + + +async def _async_setup_entity(hass, config, async_add_entities, connection): + """Set up the Node-RED Switch.""" + + switch_type = config.get(CONF_SUB_TYPE, TYPE_SWITCH) + switch_class = ( + NodeRedDeviceTrigger if switch_type == TYPE_DEVICE_TRIGGER else NodeRedSwitch + ) + async_add_entities([switch_class(hass, config, connection)]) + + +class NodeRedSwitch(ToggleEntity, NodeRedEntity): + """Node-RED Switch class.""" + + def __init__(self, hass, config, connection): + """Initialize the switch.""" + super().__init__(hass, config) + self._message_id = config[CONF_ID] + self._connection = connection + self._state = config.get(CONF_STATE, True) + self._component = CONF_SWITCH + self._available = True + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self._state + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._config.get(CONF_ICON, SWITCH_ICON) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + async def async_turn_off(self, **kwargs) -> None: + """Turn off the switch.""" + self._update_node_red(False) + + async def async_turn_on(self, **kwargs) -> None: + """Turn on the switch.""" + self._update_node_red(True) + + async def async_trigger_node(self, **kwargs) -> None: + """Trigger node in Node-RED.""" + data = {} + data[CONF_ENTITY_ID] = kwargs.get(CONF_TRIGGER_ENTITY_ID) + data[CONF_SKIP_CONDITION] = kwargs.get(CONF_SKIP_CONDITION, False) + data[CONF_OUTPUT_PATH] = kwargs.get(CONF_OUTPUT_PATH, True) + if kwargs.get(CONF_PAYLOAD) is not None: + data[CONF_PAYLOAD] = kwargs[CONF_PAYLOAD] + + self._connection.send_message( + event_message( + self._message_id, + {CONF_TYPE: EVENT_TRIGGER_NODE, CONF_DATA: data}, + ) + ) + + def _update_node_red(self, state): + self._connection.send_message( + event_message( + self._message_id, {CONF_TYPE: EVENT_STATE_CHANGED, CONF_STATE: state} + ) + ) + + @callback + def handle_lost_connection(self): + """Set availability to False when disconnected.""" + self._available = False + self.async_write_ha_state() + + @callback + def handle_discovery_update(self, msg, connection): + """Update entity config.""" + if CONF_REMOVE in msg: + # Remove entity + self.hass.async_create_task(self.async_remove()) + else: + self._available = True + self._state = msg[CONF_STATE] + self._config = msg[CONF_CONFIG] + self._message_id = msg[CONF_ID] + self._connection = connection + self._connection.subscriptions[msg[CONF_ID]] = self.handle_lost_connection + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + + self._connection.subscriptions[self._message_id] = self.handle_lost_connection + + +class NodeRedDeviceTrigger(NodeRedSwitch): + """Node-RED Device Trigger class.""" + + def __init__(self, hass, config, connection): + """Initialize the switch.""" + super().__init__(hass, config, connection) + self._trigger_config = config[CONF_DEVICE_TRIGGER] + self._unsubscribe_device_trigger = None + + @callback + def handle_lost_connection(self): + """Set remove device trigger when disconnected.""" + super().handle_lost_connection() + self.remove_device_trigger() + + async def add_device_trigger(self): + """Validate device trigger.""" + + @callback + def forward_trigger(event, context=None): + """Forward events to websocket.""" + message = event_message( + self._message_id, + {"type": EVENT_DEVICE_TRIGGER, "data": event["trigger"]}, + ) + self._connection.send_message( + json.dumps(message, cls=NodeRedJSONEncoder, allow_nan=False) + ) + + try: + trigger_config = await trigger.async_validate_trigger_config( + self.hass, [self._trigger_config] + ) + self._unsubscribe_device_trigger = await trigger.async_initialize_triggers( + self.hass, + trigger_config, + forward_trigger, + DOMAIN, + DOMAIN, + _LOGGER.log, + ) + except vol.MultipleInvalid as ex: + _LOGGER.error( + f"Error initializing device trigger '{self._node_id}': {str(ex)}", + ) + + def remove_device_trigger(self): + """Remove device trigger.""" + self._trigger_config = None + if self._unsubscribe_device_trigger is not None: + _LOGGER.info(f"removed device triger - {self._server_id} {self._node_id}") + self._unsubscribe_device_trigger() + self._unsubscribe_device_trigger = None + + @callback + async def handle_discovery_update(self, msg, connection): + """Update entity config.""" + if CONF_REMOVE not in msg and self._trigger_config != msg[CONF_DEVICE_TRIGGER]: + self.remove_device_trigger() + self._trigger_config = msg[CONF_DEVICE_TRIGGER] + await self.add_device_trigger() + + super().handle_discovery_update(msg, connection) + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + + await self.add_device_trigger() + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + self.remove_device_trigger() + await super().async_will_remove_from_hass() diff --git a/custom_components/nodered/translations/en.json b/custom_components/nodered/translations/en.json new file mode 100644 index 0000000..7db1d5b --- /dev/null +++ b/custom_components/nodered/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "title": "Node-RED Companion", + "description": "Are you sure you want to set up Node-RED Companion?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Node-RED Companion is allowed." + } + } +} diff --git a/custom_components/nodered/translations/nb.json b/custom_components/nodered/translations/nb.json new file mode 100644 index 0000000..917ebfd --- /dev/null +++ b/custom_components/nodered/translations/nb.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "title": "Node-RED", + "description": "Er du sikker på at du vil konfigurere Node-RED?" + } + }, + "abort": { + "single_instance_allowed": "Bare en enkelt konfigurasjon av Node-RED er tillatt." + } + } +} diff --git a/custom_components/nodered/utils.py b/custom_components/nodered/utils.py new file mode 100644 index 0000000..1a8df5d --- /dev/null +++ b/custom_components/nodered/utils.py @@ -0,0 +1,19 @@ +"""Helpers for node-red.""" +from datetime import timedelta +from typing import Any + +from homeassistant.helpers.json import JSONEncoder + + +class NodeRedJSONEncoder(JSONEncoder): + """JSONEncoder that supports timedelta objects and falls back to the Home Assistant Encoder.""" + + def default(self, o: Any) -> Any: + """Convert timedelta objects. + + Hand other objects to the Home Assistant JSONEncoder. + """ + if isinstance(o, timedelta): + return o.total_seconds() + + return JSONEncoder.default(self, o) diff --git a/custom_components/nodered/websocket.py b/custom_components/nodered/websocket.py new file mode 100644 index 0000000..9edbd52 --- /dev/null +++ b/custom_components/nodered/websocket.py @@ -0,0 +1,194 @@ +"""Websocket API for Node-RED.""" +import json +import logging + +import voluptuous as vol + +from homeassistant.components import device_automation +from homeassistant.components.device_automation.exceptions import ( + DeviceNotFound, + InvalidDeviceAutomationConfig, +) +from homeassistant.components.device_automation.trigger import TRIGGER_SCHEMA +from homeassistant.components.websocket_api import ( + async_register_command, + async_response, + error_message, + event_message, + require_admin, + result_message, + websocket_command, +) +from homeassistant.const import ( + CONF_DOMAIN, + CONF_ID, + CONF_NAME, + CONF_STATE, + CONF_TYPE, + CONF_WEBHOOK_ID, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + CONF_ATTRIBUTES, + CONF_COMPONENT, + CONF_CONFIG, + CONF_DEVICE_INFO, + CONF_DEVICE_TRIGGER, + CONF_NODE_ID, + CONF_REMOVE, + CONF_SERVER_ID, + CONF_SUB_TYPE, + DOMAIN, + NODERED_DISCOVERY, + NODERED_ENTITY, + VERSION, +) + +_LOGGER = logging.getLogger(__name__) + + +def register_websocket_handlers(hass: HomeAssistantType): + """Register the websocket handlers.""" + + async_register_command(hass, websocket_version) + async_register_command(hass, websocket_webhook) + async_register_command(hass, websocket_discovery) + async_register_command(hass, websocket_entity) + async_register_command(hass, websocket_device_action) + + +@require_admin +@async_response +@websocket_command( + { + vol.Required(CONF_TYPE): "nodered/device_action", + vol.Required("action"): cv.DEVICE_ACTION_SCHEMA, + } +) +async def websocket_device_action(hass, connection, msg): + """Sensor command.""" + context = connection.context(msg) + platform = await device_automation.async_get_device_automation_platform( + hass, msg["action"][CONF_DOMAIN], "action" + ) + + try: + await platform.async_call_action_from_config(hass, msg["action"], {}, context) + connection.send_message(result_message(msg[CONF_ID], {"success": True})) + except InvalidDeviceAutomationConfig as err: + connection.send_message(error_message(msg[CONF_ID], "invalid_config", str(err))) + except DeviceNotFound as err: + connection.send_message( + error_message(msg[CONF_ID], "device_not_found", str(err)) + ) + + +@require_admin +@websocket_command( + { + vol.Required(CONF_TYPE): "nodered/discovery", + vol.Required(CONF_COMPONENT): cv.string, + vol.Required(CONF_SERVER_ID): cv.string, + vol.Required(CONF_NODE_ID): cv.string, + vol.Optional(CONF_CONFIG, default={}): dict, + vol.Optional(CONF_STATE): vol.Any(bool, str, int, float), + vol.Optional(CONF_ATTRIBUTES): dict, + vol.Optional(CONF_REMOVE): bool, + vol.Optional(CONF_DEVICE_INFO): dict, + vol.Optional(CONF_DEVICE_TRIGGER): TRIGGER_SCHEMA, + vol.Optional(CONF_SUB_TYPE): str, + } +) +def websocket_discovery(hass, connection, msg): + """Sensor command.""" + async_dispatcher_send( + hass, NODERED_DISCOVERY.format(msg[CONF_COMPONENT]), msg, connection + ) + connection.send_message(result_message(msg[CONF_ID], {"success": True})) + + +@require_admin +@websocket_command( + { + vol.Required(CONF_TYPE): "nodered/entity", + vol.Required(CONF_SERVER_ID): cv.string, + vol.Required(CONF_NODE_ID): cv.string, + vol.Required(CONF_STATE): vol.Any(bool, str, int, float), + vol.Optional(CONF_ATTRIBUTES, default={}): dict, + } +) +def websocket_entity(hass, connection, msg): + """Sensor command.""" + + async_dispatcher_send( + hass, NODERED_ENTITY.format(msg[CONF_SERVER_ID], msg[CONF_NODE_ID]), msg + ) + connection.send_message(result_message(msg[CONF_ID], {"success": True})) + + +@require_admin +@websocket_command({vol.Required(CONF_TYPE): "nodered/version"}) +def websocket_version(hass, connection, msg): + """Version command.""" + + connection.send_message(result_message(msg[CONF_ID], VERSION)) + + +@require_admin +@async_response +@websocket_command( + { + vol.Required(CONF_TYPE): "nodered/webhook", + vol.Required(CONF_WEBHOOK_ID): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_SERVER_ID): cv.string, + } +) +async def websocket_webhook(hass, connection, msg): + """Create webhook command.""" + webhook_id = msg[CONF_WEBHOOK_ID] + + @callback + async def handle_webhook(hass, id, request): + """Handle webhook callback.""" + body = await request.text() + try: + payload = json.loads(body) if body else {} + except ValueError: + payload = body + + data = { + "payload": payload, + "headers": dict(request.headers), + "params": dict(request.query), + } + + _LOGGER.debug(f"Webhook received {id[:15]}..: {data}") + connection.send_message(event_message(msg[CONF_ID], {"data": data})) + + def remove_webhook() -> None: + """Remove webhook command.""" + try: + hass.components.webhook.async_unregister(webhook_id) + + except ValueError: + pass + + _LOGGER.info(f"Webhook removed: {webhook_id[:15]}..") + connection.send_message(result_message(msg[CONF_ID], {"success": True})) + + try: + hass.components.webhook.async_register( + DOMAIN, msg[CONF_NAME], webhook_id, handle_webhook + ) + except ValueError: + connection.send_message(result_message(msg[CONF_ID], {"success": False})) + return + + _LOGGER.info(f"Webhook created: {webhook_id[:15]}..") + connection.subscriptions[msg[CONF_ID]] = remove_webhook + connection.send_message(result_message(msg[CONF_ID], {"success": True})) diff --git a/entities/input_datetime/.gitkeep b/customizations/domains/.gitkeep similarity index 100% rename from entities/input_datetime/.gitkeep rename to customizations/domains/.gitkeep diff --git a/entities/input_selects/.gitkeep b/customizations/entities/.gitkeep similarity index 100% rename from entities/input_selects/.gitkeep rename to customizations/entities/.gitkeep diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index a113283..0000000 --- a/docs/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Home Assistant Configuration Files - -[![Build Status](https://travis-ci.org/jonathanadams/Home-Assistant-Configuration.svg?branch=master)](https://travis-ci.org/jonathanadams/Home-Assistant-Configuration) -![GitHub stars](https://img.shields.io/github/stars/jonathanadams/Home-Assistant-Configuration.svg) -![GitHub commit activity](https://img.shields.io/github/commit-activity/m/jonathanadams/Home-Assistant-Configuration.svg) -![GitHub last commit (branch)](https://img.shields.io/github/last-commit/jonathanadams/Home-Assistant-Configuration/master.svg) - -This is my currently active set of configuration files for my Home Assistant. If you are interested in following my progress be sure to ⭐️ star this repository or watch to receive notifications. \ No newline at end of file diff --git a/entities/switches/.gitkeep b/entities/binary_sensors/.gitkeep similarity index 100% rename from entities/switches/.gitkeep rename to entities/binary_sensors/.gitkeep diff --git a/entities/binary_sensors/hikvision_nvr.yaml.disable b/entities/binary_sensors/hikvision_nvr.yaml.disable deleted file mode 100644 index 98982f1..0000000 --- a/entities/binary_sensors/hikvision_nvr.yaml.disable +++ /dev/null @@ -1,5 +0,0 @@ -platform: hikvision -host: 10.1.3.2 -port: 80 -username: !secret camera_nvr_username -password: !secret camera_nvr_password \ No newline at end of file diff --git a/entities/binary_sensors/ring.yaml b/entities/binary_sensors/ring.yaml deleted file mode 100644 index f963862..0000000 --- a/entities/binary_sensors/ring.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Ring Doorbell -# https://www.home-assistant.io/integrations/ring/ - -# DISABLED: Currently disabled due to error - -platform: ring \ No newline at end of file diff --git a/entities/input_booleans/alarm_arming_state_notifications.yaml b/entities/input_booleans/alarm_arming_state_notifications.yaml deleted file mode 100644 index 13ba12d..0000000 --- a/entities/input_booleans/alarm_arming_state_notifications.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Alarm Arming Status Notifications - -notification_alarm_arming_status: - name: Alarm Arming Status Notifications - initial: on diff --git a/entities/input_booleans/alarm_sounding_notifications.yaml b/entities/input_booleans/alarm_sounding_notifications.yaml deleted file mode 100644 index cb69934..0000000 --- a/entities/input_booleans/alarm_sounding_notifications.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Alarm Sounding Notifications - -notification_alarm_sounding: - name: Alarm Sounding Notifications - initial: on diff --git a/entities/input_booleans/alarm_sounding_sms_notification.yaml b/entities/input_booleans/alarm_sounding_sms_notification.yaml deleted file mode 100644 index 2b7a761..0000000 --- a/entities/input_booleans/alarm_sounding_sms_notification.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Alarm Sounding SMS Notification - -notification_sms_alarm_sounding: - name: Alarm Sounding SMS Alerts - initial: on diff --git a/entities/input_booleans/away_mode.yaml b/entities/input_booleans/away_mode.yaml deleted file mode 100644 index 4b6d674..0000000 --- a/entities/input_booleans/away_mode.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Guest Mode - -away_mode: - name: Away Mode - initial: false - icon: mdi:beach diff --git a/entities/input_booleans/garden_decking_timer_automation.yaml b/entities/input_booleans/garden_decking_timer_automation.yaml deleted file mode 100644 index aa9438e..0000000 --- a/entities/input_booleans/garden_decking_timer_automation.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Garden Decking Light Timer Automation - -garden_decking_light_timer: - name: Garden Decling Lights Timer - initial: on diff --git a/entities/input_booleans/guest_mode.yaml b/entities/input_booleans/guest_mode.yaml deleted file mode 100644 index d808a18..0000000 --- a/entities/input_booleans/guest_mode.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Guest Mode - -guest_mode: - name: Guest Mode - initial: false - icon: mdi:hotel diff --git a/entities/input_booleans/porch_lighting_timer.yaml b/entities/input_booleans/porch_lighting_timer.yaml deleted file mode 100644 index ab5e6ec..0000000 --- a/entities/input_booleans/porch_lighting_timer.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Porch Lighting Timer - -porch_lighting_timer: - name: Porch Lighting Timer - initial: on diff --git a/scripts/.gitkeep b/entities/lights/.gitkeep similarity index 100% rename from scripts/.gitkeep rename to entities/lights/.gitkeep diff --git a/entities/lights/living_room_all_lamps.yaml b/entities/lights/living_room_all_lamps.yaml deleted file mode 100644 index 09853ce..0000000 --- a/entities/lights/living_room_all_lamps.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# Living Room - All Lamps -# https://www.home-assistant.io/integrations/light.group/ - -platform: group -name: living_room_all_lamps -entities: - - light.living_room_left_lamp - - light.living_room_right_lamp - - light.living_room_tall_lamp diff --git a/entities/lights/living_room_all_lights.yaml b/entities/lights/living_room_all_lights.yaml deleted file mode 100644 index fc5b2c9..0000000 --- a/entities/lights/living_room_all_lights.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -# Living Room - All Lights -# https://www.home-assistant.io/integrations/light.group/ - -platform: group -name: living_room_all_lights -entities: - - light.living_room_all_lamps - - light.living_room_wall_lights diff --git a/entities/lights/living_room_wall_lights.yaml b/entities/lights/living_room_wall_lights.yaml deleted file mode 100644 index 7bd9f4b..0000000 --- a/entities/lights/living_room_wall_lights.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -# Living Room - Wall Lights -# https://www.home-assistant.io/integrations/light.group/ - -platform: group -name: living_room_wall_lights -entities: - - light.living_room_left_color - - light.living_room_right_color diff --git a/entities/lights/master_bedroom_all_lamps.yaml b/entities/lights/master_bedroom_all_lamps.yaml deleted file mode 100644 index 2d747e3..0000000 --- a/entities/lights/master_bedroom_all_lamps.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -# Master Bedroom - All Lamps -# https://www.home-assistant.io/integrations/light.group/ - -platform: group -name: master_bedroom_all_lamps -entities: - - light.master_bedroom_jon_bedside_lamp - - light.master_bedroom_lee_bedside_lamp diff --git a/entities/lights/master_bedroom_all_lights.yaml b/entities/lights/master_bedroom_all_lights.yaml deleted file mode 100644 index 3e0ccb8..0000000 --- a/entities/lights/master_bedroom_all_lights.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Living Room - All Lights -# https://www.home-assistant.io/integrations/light.group/ - -platform: group -name: master_bedroom_all_lights -entities: - - light.master_bedroom_all_lamps diff --git a/entities/lights/office_all_lamps.yaml b/entities/lights/office_all_lamps.yaml deleted file mode 100644 index 9a20656..0000000 --- a/entities/lights/office_all_lamps.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -# Office - All Lamps -# https://www.home-assistant.io/integrations/light.group/ - -platform: group -name: office_all_lamps -entities: - - light.office_jon_desk_lamp - - light.office_lee_desk_lamp diff --git a/entities/lights/office_all_lights.yaml b/entities/lights/office_all_lights.yaml deleted file mode 100644 index 1d1459d..0000000 --- a/entities/lights/office_all_lights.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -# Office - All Lights -# https://www.home-assistant.io/integrations/light.group/ - -platform: group -name: office_all_lights -entities: - - light.office_ceiling_light - - light.office_all_lamps diff --git a/themes/.gitkeep b/entities/media_players/.gitkeep similarity index 100% rename from themes/.gitkeep rename to entities/media_players/.gitkeep diff --git a/entities/media_players/living_room_sony_bravia_tv.yaml b/entities/media_players/living_room_sony_bravia_tv.yaml deleted file mode 100644 index 8564328..0000000 --- a/entities/media_players/living_room_sony_bravia_tv.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Sony Bravia TV -# https://www.home-assistant.io/components/braviatv/ - -platform: braviatv -name: Living Room TV -host: !secret living_room_tv_ip diff --git a/entities/media_players/living_room_yamaha_amplifier.yaml b/entities/media_players/living_room_yamaha_amplifier.yaml deleted file mode 100644 index 8217183..0000000 --- a/entities/media_players/living_room_yamaha_amplifier.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -# Living Room Yamaha Amplifier -# https://www.home-assistant.io/components/yamaha/ - -platform: yamaha -name: Living Room Amplifier -host: !secret living_room_amp_ip -source_names: - HDMI1: "HDMI1 - Sky Q" -source_ignore: - - "Deezer" - - "JUKE" - - "MusicCast Link" - - "Qobuz" - - "Spotify" - - "TUNER" - - "TIDAL" - - "USB" - - "AUDIO1" - - "AUDIO2" - - "AUDIO3" - - "AV1" - - "AV2" - - "AV3" - - "AUX" - - "SERVER" diff --git a/entities/notifiers/.gitkeep b/entities/notifiers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/entities/notify/family.yaml b/entities/notify/family.yaml deleted file mode 100644 index 21cd72e..0000000 --- a/entities/notify/family.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Defines notification group for all family - -name: family -platform: group -services: - - service: jon_devices - - service: lee_devices diff --git a/entities/notify/jon_devices.yaml b/entities/notify/jon_devices.yaml deleted file mode 100644 index 15aadf0..0000000 --- a/entities/notify/jon_devices.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Defines notification group for Jon's Devices - -name: jon_devices -platform: group -services: - - service: mobile_app_jonathans_iphone_x - - service: mobile_app_jonathans_ipad diff --git a/entities/notify/lee_devices.yaml b/entities/notify/lee_devices.yaml deleted file mode 100644 index 5277b9e..0000000 --- a/entities/notify/lee_devices.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Defines notification group for Lee's Devices - -name: lee_devices -platform: group -services: - - service: mobile_app_lees_iphone_x - - service: mobile_app_lees_ipad diff --git a/entities/notify/living_room_tv.yaml b/entities/notify/living_room_tv.yaml deleted file mode 100644 index 1d89b28..0000000 --- a/entities/notify/living_room_tv.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Living Room TV Notification - -platform: nfandroidtv -name: Living Room TV -host: !secret living_room_tv_ip diff --git a/entities/notify/sms_notification.yaml b/entities/notify/sms_notification.yaml deleted file mode 100644 index d3fc87b..0000000 --- a/entities/notify/sms_notification.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# SMS Notification - -name: sms_notification -platform: twilio_sms -from_number: 17RowanCl diff --git a/entities/persons/jon.yaml b/entities/persons/jon.yaml deleted file mode 100644 index 0256333..0000000 --- a/entities/persons/jon.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: Jon -id: jon -user_id: 7d12341fc1434391b090c88ba0cdd8e3 -device_trackers: - - device_tracker.jonathans_iphone_x diff --git a/entities/persons/lee.yaml b/entities/persons/lee.yaml deleted file mode 100644 index fef9e52..0000000 --- a/entities/persons/lee.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: Lee -id: lee -user_id: b8d967ea2f044aa5a00f8b76d653fb5b -device_trackers: - - device_tracker.lees_iphone_x diff --git a/entities/sensors/.gitkeep b/entities/sensors/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/entities/sensors/bathroom_radiator_faults.yaml b/entities/sensors/bathroom_radiator_faults.yaml deleted file mode 100644 index f100471..0000000 --- a/entities/sensors/bathroom_radiator_faults.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Bathroom Radiator Faults Sensor - -platform: template -sensors: - bathroom_radiator_faults: - friendly_name: "Bathroom Radiator Faults" - value_template: >- - {% if state_attr('climate.bathroom', 'status').active_faults %} - {% if state_attr('climate.bathroom', 'status').active_faults[0].fault_type == 'TempZoneActuatorLowBattery' %} - Low Battery - {% endif %} - Fault Detected - {% else %} - Functioning Normally - {% endif %} - icon_template: >- - {% set fault_level = states('sensor.bathroom_radiator_faults') %} - {% if fault_level == 'Low Battery' %} - mdi:alert - {% elif fault_level == 'Fault Detected' %} - mdi:alert - {% else %} - mdi:check-circle - {% endif %} diff --git a/entities/sensors/hallway_radiator_faults.yaml b/entities/sensors/hallway_radiator_faults.yaml deleted file mode 100644 index 9ba07f5..0000000 --- a/entities/sensors/hallway_radiator_faults.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Hallway Radiator Faults Sensor - -platform: template -sensors: - hallway_radiator_faults: - friendly_name: "Hallway Radiator Faults" - value_template: >- - {% if state_attr('climate.hallway', 'status').active_faults %} - {% if state_attr('climate.hallway', 'status').active_faults[0].fault_type == 'TempZoneActuatorLowBattery' %} - Low Battery - {% endif %} - Fault Detected - {% else %} - Functioning Normally - {% endif %} - icon_template: >- - {% set fault_level = states('sensor.hallway_radiator_faults') %} - {% if fault_level == 'Low Battery' %} - mdi:alert - {% elif fault_level == 'Fault Detected' %} - mdi:alert - {% else %} - mdi:check-circle - {% endif %} diff --git a/entities/sensors/kitchen_radiator_faults.yaml b/entities/sensors/kitchen_radiator_faults.yaml deleted file mode 100644 index c1c5398..0000000 --- a/entities/sensors/kitchen_radiator_faults.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Kitchen Radiator Faults Sensor - -platform: template -sensors: - kitchen_radiator_faults: - friendly_name: "Kitchen Radiator Faults" - value_template: >- - {% if state_attr('climate.kitchen', 'status').active_faults %} - {% if state_attr('climate.kitchen', 'status').active_faults[0].fault_type == 'TempZoneActuatorLowBattery' %} - Low Battery - {% endif %} - Fault Detected - {% else %} - Functioning Normally - {% endif %} - icon_template: >- - {% set fault_level = states('sensor.kitchen_radiator_faults') %} - {% if fault_level == 'Low Battery' %} - mdi:alert - {% elif fault_level == 'Fault Detected' %} - mdi:alert - {% else %} - mdi:check-circle - {% endif %} diff --git a/entities/sensors/living_room_radiator_faults.yaml b/entities/sensors/living_room_radiator_faults.yaml deleted file mode 100644 index d08037c..0000000 --- a/entities/sensors/living_room_radiator_faults.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Living Room Radiator Faults Sensor - -platform: template -sensors: - livingroom_radiator_faults: - friendly_name: "Living Room Radiator Faults" - value_template: >- - {% if state_attr('climate.living_room', 'status').active_faults %} - {% if state_attr('climate.living_room', 'status').active_faults[0].fault_type == 'TempZoneActuatorLowBattery' %} - Low Battery - {% endif %} - Fault Detected - {% else %} - Functioning Normally - {% endif %} - icon_template: >- - {% set fault_level = states('sensor.livingroom_radiator_faults') %} - {% if fault_level == 'Low Battery' %} - mdi:alert - {% elif fault_level == 'Fault Detected' %} - mdi:alert - {% else %} - mdi:check-circle - {% endif %} diff --git a/entities/sensors/master_bedroom_radiator_faults.yaml b/entities/sensors/master_bedroom_radiator_faults.yaml deleted file mode 100644 index a18560c..0000000 --- a/entities/sensors/master_bedroom_radiator_faults.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Master Bedroom Radiator Faults Sensor - -platform: template -sensors: - master_bedroom_radiator_faults: - friendly_name: "Master Bedroom Radiator Faults" - value_template: >- - {% if state_attr('climate.master_bedroom', 'status').active_faults %} - {% if state_attr('climate.master_bedroom', 'status').active_faults[0].fault_type == 'TempZoneActuatorLowBattery' %} - Low Battery - {% endif %} - Fault Detected - {% else %} - Functioning Normally - {% endif %} - icon_template: >- - {% set fault_level = states('sensor.master_bedroom_radiator_faults') %} - {% if fault_level == 'Low Battery' %} - mdi:alert - {% elif fault_level == 'Fault Detected' %} - mdi:alert - {% else %} - mdi:check-circle - {% endif %} diff --git a/entities/sensors/office_radiator_faults.yaml b/entities/sensors/office_radiator_faults.yaml deleted file mode 100644 index acb48dd..0000000 --- a/entities/sensors/office_radiator_faults.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Office Radiator Faults Sensor - -platform: template -sensors: - office_radiator_faults: - friendly_name: "Office Radiator Faults" - value_template: >- - {% if state_attr('climate.office', 'status').active_faults %} - {% if state_attr('climate.office', 'status').active_faults[0].fault_type == 'TempZoneActuatorLowBattery' %} - Low Battery - {% endif %} - Fault Detected - {% else %} - Functioning Normally - {% endif %} - icon_template: >- - {% set fault_level = states('sensor.office_radiator_faults') %} - {% if fault_level == 'Low Battery' %} - mdi:alert - {% elif fault_level == 'Fault Detected' %} - mdi:alert - {% else %} - mdi:check-circle - {% endif %} diff --git a/entities/sensors/ring.yaml b/entities/sensors/ring.yaml deleted file mode 100644 index bd83350..0000000 --- a/entities/sensors/ring.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Ring -# https://www.home-assistant.io/integrations/ring/ - -# DISABLED: Currently disabled due to error - -platform: ring \ No newline at end of file diff --git a/entities/sensors/spare_room_radiator_faults.yaml b/entities/sensors/spare_room_radiator_faults.yaml deleted file mode 100644 index c5d44b7..0000000 --- a/entities/sensors/spare_room_radiator_faults.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Spare Room Radiator Faults Sensor - -platform: template -sensors: - spare_room_radiator_faults: - friendly_name: "Spare Room Radiator Faults" - value_template: >- - {% if state_attr('climate.spare_room', 'status').active_faults %} - {% if state_attr('climate.spare_room', 'status').active_faults[0].fault_type == 'TempZoneActuatorLowBattery' %} - Low Battery - {% endif %} - Fault Detected - {% else %} - Functioning Normally - {% endif %} - icon_template: >- - {% set fault_level = states('sensor.spare_room_radiator_faults') %} - {% if fault_level == 'Low Battery' %} - mdi:alert - {% elif fault_level == 'Fault Detected' %} - mdi:alert - {% else %} - mdi:check-circle - {% endif %} diff --git a/entities/sensors/travisci.yaml b/entities/sensors/travisci.yaml deleted file mode 100644 index 64742f3..0000000 --- a/entities/sensors/travisci.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Travis CI Sensor -# https://www.home-assistant.io/components/travisci/ - -platform: travisci -api_key: !secret travisci_api_key -repository: "Home-Assistant-Configuration" -monitored_conditions: - - last_build_id - - last_build_state diff --git a/entities/sensors/version_avaliable.yaml.disable b/entities/sensors/version_avaliable.yaml.disable deleted file mode 100644 index 17c77f9..0000000 --- a/entities/sensors/version_avaliable.yaml.disable +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Sensor for displaying avaliable version of Home Assistant -# https://www.home-assistant.io/integrations/sensor.version/ - -platform: version -name: latest_version -source: hassio -image: qemux86-64 diff --git a/entities/sensors/version_current.yaml.disable b/entities/sensors/version_current.yaml.disable deleted file mode 100644 index 76f601c..0000000 --- a/entities/sensors/version_current.yaml.disable +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Sensor for displaying current version of Home Assistant -# https://www.home-assistant.io/integrations/sensor.version/ - -platform: version -name: version_current -source: local diff --git a/entities/weather/darksky.yaml b/entities/weather/darksky.yaml deleted file mode 100644 index f728d8e..0000000 --- a/entities/weather/darksky.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# Darksky -# https://www.home-assistant.io/integrations/weather.darksky/ - -platform: darksky -api_key: !secret darksky_api_key -mode: daily -units: uk -latitude: !secret home_latitude -longitude: !secret home_longitude \ No newline at end of file diff --git a/entities/zones/home.yaml b/entities/zones/home.yaml deleted file mode 100644 index 76b433b..0000000 --- a/entities/zones/home.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# Defines the Home Zone -# https://www.home-assistant.io/integrations/zone/ - -name: Home -latitude: !secret home_latitude -longitude: !secret home_longitude -radius: 400 -passive: false -icon: mdi:home-map-marker diff --git a/entities/zones/home_ringfence.yaml b/entities/zones/home_ringfence.yaml deleted file mode 100644 index 18a0b2c..0000000 --- a/entities/zones/home_ringfence.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# Defines the Home Zone -# https://www.home-assistant.io/integrations/zone/ - -name: Home (Ringfence) -latitude: !secret home_latitude -longitude: !secret home_longitude -radius: 400 -passive: true -icon: mdi:home-map-marker diff --git a/esphome/.gitignore b/esphome/.gitignore deleted file mode 100644 index 0bbf89c..0000000 --- a/esphome/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Gitignore settings for ESPHome -# This is an example and may include too much for your use-case. -# You can modify this file to suit your needs. -/.esphome/ -**/.pioenvs/ -**/.piolibdeps/ -**/lib/ -**/src/ -**/platformio.ini -/secrets.yaml diff --git a/esphome/alarm_system_status.yaml b/esphome/alarm_system_status.yaml deleted file mode 100644 index b31d7f2..0000000 --- a/esphome/alarm_system_status.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# Custom ESP8266 Device - -# Basic Config -esphome: - name: alarm_system_status - platform: ESP8266 - board: d1_mini - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO12 - inverted: true - id: alarm_arming_staus - - platform: gpio - pin: - number: GPIO14 - inverted: true - id: alarm_sounder_status - -text_sensor: - - platform: template - name: "Alarm Arming Status" - icon: "mdi:lock" - lambda: |- - if (id(alarm_arming_staus).state == 1) { - return {"Armed"}; - } else { - return {"Disarmed"}; - } - update_interval: 5s - - platform: template - name: "Alarm Sounder Status" - icon: "mdi:alarm-bell" - lambda: |- - if (id(alarm_sounder_status).state == 1) { - return {"Sounding"}; - } else { - return {"Not Sounding"}; - } - update_interval: 5s - -sensor: - - platform: wifi_signal - name: "Alarm System WiFi Status" - update_interval: 60s - -switch: - - platform: restart - name: "Alarm System Status Restart" \ No newline at end of file diff --git a/esphome/christmas_2.yaml b/esphome/christmas_2.yaml deleted file mode 100644 index 87771a1..0000000 --- a/esphome/christmas_2.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Sonoff S20 - -# Substitutions -substitutions: - # Device Names - device_name: "christmas_2" - friendly_name: "Christmas 2" - # Comment / Info To Display In ESPHome Front End - device_comment: "" - # Icon - main_icon: "ornament" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "RESTORE_DEFAULT_OFF" - -# Basic Config -esphome: - name: "${device_name}" - comment: "${device_comment}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Status" - -sensor: - - platform: wifi_signal - name: "${friendly_name} WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO12 - id: relay - restore_mode: "${default_state}" - -status_led: - pin: - number: GPIO13 \ No newline at end of file diff --git a/esphome/dishwasher.yaml b/esphome/dishwasher.yaml deleted file mode 100644 index 1eb095e..0000000 --- a/esphome/dishwasher.yaml +++ /dev/null @@ -1,161 +0,0 @@ -# Maxcio W-UK007S - -# Substitutions -substitutions: - # Device Names - device_name: "dishwasher" - friendly_name: "Dishwasher" - # Icon - main_icon: "dishwasher" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "ALWAYS_ON" - # Activity State Threshold - # Threshold (number) that the device will change from `Idle` to `Active` if power is greater than or equal to - activity_threshold: "5" - -# Basic Config -esphome: - name: "${device_name}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - # Push Button (Toggles Relay When Pressed) - - platform: gpio - pin: - number: GPIO13 - mode: INPUT_PULLUP - inverted: true - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Server Status" - -switch: - # Relay (As Switch) - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO15 - id: relay - restore_mode: "${default_state}" - on_turn_on: - - light.turn_on: - id: led - brightness: 100% - transition_length: 0s - on_turn_off: - - light.turn_off: - id: led - transition_length: 0s - -sensor: - # WiFi Signal Sensor - - platform: wifi_signal - name: "${friendly_name} WiFi Status" - update_interval: 60s - # Power Monitoring - - platform: hlw8012 - sel_pin: - number: GPIO12 - inverted: true - cf_pin: GPIO5 - cf1_pin: GPIO14 - change_mode_every: 3 - update_interval: 3s - voltage: - name: "${friendly_name} Voltage" - id: voltage - unit_of_measurement: V - accuracy_decimals: 1 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.0 - - 602.87506 -> 229.9 - - 609.8 -> 232.8 - power: - name: "${friendly_name} Power" - id: power - unit_of_measurement: W - accuracy_decimals: 0 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 1.14 - - 62.06167 -> 10.93 - - 1503.27161 -> 247.6 - - 1599.81213 -> 263.7 - - 3923.67700 -> 631.4 - - 7109.50928 -> 1148.0 - - 7237.0857 -> 1193.0 - - 7426.71338 -> 1217.0 - current: - name: "${friendly_name} Current" - id: current - unit_of_measurement: A - accuracy_decimals: 3 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.013 - - 0.08208 -> 0.071 - - 1.34223 -> 1.066 - - 5.57170 -> 4.408 - - 6.69184 -> 5.259 - - 6.97187 -> 5.540 - # Total daily energy sensor - - platform: total_daily_energy - name: "${friendly_name} Daily Energy" - power_id: power - filters: - # Multiplication factor from W to kW is 0.001 - - multiply: 0.001 - unit_of_measurement: kWh - -text_sensor: - # Device Activity State - # e.g. shows as active if power reading above a certain threshold - - platform: template - name: "${friendly_name} Activity State" - icon: "mdi:${main_icon}" - lambda: |- - if (id(power).state >= $activity_threshold) { - return {"Active"}; - } else { - return {"Idle"}; - } - update_interval: 5s - -# Relay State LED -output: - - platform: esp8266_pwm - id: state_led - pin: - number: GPIO0 - inverted: true - -light: - - platform: monochromatic - output: state_led - id: led \ No newline at end of file diff --git a/esphome/garden_decking_lights.yaml b/esphome/garden_decking_lights.yaml deleted file mode 100644 index c4e0abb..0000000 --- a/esphome/garden_decking_lights.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Sonoff S20 - -# Basic Config -esphome: - name: garden_decking_lights - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "Garden Decking Lights Button" - on_press: - - switch.toggle: relay - - platform: status - name: "Garden Decking Lights Status" - -sensor: - - platform: wifi_signal - name: "Garden Decking Lights WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "Garden Decking Lights" - pin: GPIO12 - id: relay - -status_led: - pin: GPIO13 \ No newline at end of file diff --git a/esphome/landing_ceiling_light.yaml b/esphome/landing_ceiling_light.yaml deleted file mode 100644 index c82bf89..0000000 --- a/esphome/landing_ceiling_light.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Shelly 1 - -# Basic Config - -esphome: - name: landing_ceiling_light - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config - -binary_sensor: - - platform: status - name: "Landing Ceiling Light Status" - - platform: gpio - pin: - number: GPIO5 - #mode: INPUT_PULLUP - #inverted: True - name: "Landing Ceiling Light" - on_state: - - light.toggle: lightid - internal: true - -sensor: - - platform: wifi_signal - name: "Landing Ceiling Light WiFi Signal" - update_interval: 60s - - -output: - - platform: gpio - pin: GPIO4 - id: gpio_4 - -light: - - platform: binary - name: "Landing Ceiling Light" - output: gpio_4 - id: lightid \ No newline at end of file diff --git a/esphome/living_room_air_freshener.yaml b/esphome/living_room_air_freshener.yaml deleted file mode 100644 index 4484c42..0000000 --- a/esphome/living_room_air_freshener.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Sonoff S20 - -# Substitutions -substitutions: - # Device Names - device_name: "living_room_air_freshener" - friendly_name: "Living Room Air Freshener" - # Comment / Info To Display In ESPHome Front End - device_comment: "" - # Icon - main_icon: "spray" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "RESTORE_DEFAULT_OFF" - -# Basic Config -esphome: - name: "${device_name}" - comment: "${device_comment}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Status" - -sensor: - - platform: wifi_signal - name: "${friendly_name} WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO12 - id: relay - restore_mode: "${default_state}" - -status_led: - pin: - number: GPIO13 \ No newline at end of file diff --git a/esphome/living_room_left_lamp.yaml b/esphome/living_room_left_lamp.yaml deleted file mode 100644 index 31a5329..0000000 --- a/esphome/living_room_left_lamp.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Sonoff S20 - -# Basic Config -esphome: - name: living_room_left_lamp - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "Living Room Left Lamp Button" - on_press: - - switch.toggle: relay - - platform: status - name: "Living Room Left Lamp Status" - -sensor: - - platform: wifi_signal - name: "Living Room Left Lamp WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "Living Room Left Lamp" - pin: GPIO12 - id: relay - -status_led: - pin: - number: GPIO13 \ No newline at end of file diff --git a/esphome/living_room_right_lamp.yaml b/esphome/living_room_right_lamp.yaml deleted file mode 100644 index 8fda4fd..0000000 --- a/esphome/living_room_right_lamp.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Sonoff S20 - -# Basic Config -esphome: - name: living_room_right_lamp - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "Living Room Right Lamp Button" - on_press: - - switch.toggle: relay - - platform: status - name: "Living Room Right Lamp Status" - -sensor: - - platform: wifi_signal - name: "Living Room Right Lamp WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "Living Room Right Lamp" - pin: GPIO12 - id: relay - -status_led: - pin: GPIO13 \ No newline at end of file diff --git a/esphome/master_bedroom_jon_lamp.yaml b/esphome/master_bedroom_jon_lamp.yaml deleted file mode 100644 index a8c6cfd..0000000 --- a/esphome/master_bedroom_jon_lamp.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Sonoff S20 - -# Basic Config -esphome: - name: master_bedroom_jon_lamp - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "Master Bedroom Jon Lamp Button" - on_press: - - switch.toggle: relay - - platform: status - name: "Master Bedroom Jon Lamp Status" - -sensor: - - platform: wifi_signal - name: "Master Bedroom Jon Lamp WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "Master Bedroom Jon Lamp" - pin: GPIO12 - id: relay - -status_led: - pin: GPIO13 \ No newline at end of file diff --git a/esphome/office_ceiling_light.yaml b/esphome/office_ceiling_light.yaml deleted file mode 100644 index 450ee8a..0000000 --- a/esphome/office_ceiling_light.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Shelly 1 - -# Basic Config - -esphome: - name: office_ceiling_light - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config - -binary_sensor: - - platform: status - name: "Office Ceiling Light Status" - - platform: gpio - pin: - number: GPIO5 - #mode: INPUT_PULLUP - #inverted: True - name: "Office Ceiling Light" - on_state: - - light.toggle: lightid - internal: true - -sensor: - - platform: wifi_signal - name: "Office Ceiling Light WiFi Signal" - update_interval: 60s - - -output: - - platform: gpio - pin: GPIO4 - id: gpio_4 - -light: - - platform: binary - name: "Office Ceiling Light" - output: gpio_4 - id: lightid \ No newline at end of file diff --git a/esphome/porch_ceiling_light.yaml b/esphome/porch_ceiling_light.yaml deleted file mode 100644 index b3a5d59..0000000 --- a/esphome/porch_ceiling_light.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Shelly 1 - -# Basic Config - -esphome: - name: porch_ceiling_light - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config - -binary_sensor: - - platform: status - name: "Porch Ceiling Light Status" - - platform: gpio - pin: - number: GPIO5 - #mode: INPUT_PULLUP - #inverted: True - name: "Porch Ceiling Light" - on_state: - - light.toggle: lightid - internal: true - -sensor: - - platform: wifi_signal - name: "Porch Ceiling Light WiFi Signal" - update_interval: 60s - - -output: - - platform: gpio - pin: GPIO4 - id: gpio_4 - -light: - - platform: binary - name: "Porch Ceiling Light" - output: gpio_4 - id: lightid \ No newline at end of file diff --git a/esphome/spare_bedroom_lamp.yaml b/esphome/spare_bedroom_lamp.yaml deleted file mode 100644 index 4fc6747..0000000 --- a/esphome/spare_bedroom_lamp.yaml +++ /dev/null @@ -1,65 +0,0 @@ -# Sonoff S20 - -# Substitutions -substitutions: - # Device Names - device_name: "spare_bedroom_lamp" - friendly_name: "Spare Bedroom Lamp" - # Icon - main_icon: "floor-lamp" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "RESTORE_DEFAULT_OFF" - -# Basic Config -esphome: - name: "${device_name}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Status" - -sensor: - - platform: wifi_signal - name: "${friendly_name} WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO12 - id: relay - restore_mode: "${default_state}" - -status_led: - pin: - number: GPIO13 \ No newline at end of file diff --git a/esphome/spare_room_ceiling_light.yaml b/esphome/spare_room_ceiling_light.yaml deleted file mode 100644 index 8d33d20..0000000 --- a/esphome/spare_room_ceiling_light.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Shelly 1 - -# Basic Config - -esphome: - name: spare_room_ceiling_light - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config - -binary_sensor: - - platform: status - name: "Spare Room Ceiling Light Status" - - platform: gpio - pin: - number: GPIO5 - #mode: INPUT_PULLUP - #inverted: True - name: "Spare Room Ceiling Light" - on_state: - - light.toggle: lightid - internal: true - -sensor: - - platform: wifi_signal - name: "Spare Room Ceiling Light WiFi Signal" - update_interval: 60s - - -output: - - platform: gpio - pin: GPIO4 - id: gpio_4 - -light: - - platform: binary - name: "Spare Room Ceiling Light" - output: gpio_4 - id: lightid \ No newline at end of file diff --git a/esphome/test_energy_monitor_plug.yaml b/esphome/test_energy_monitor_plug.yaml deleted file mode 100644 index d00e7f0..0000000 --- a/esphome/test_energy_monitor_plug.yaml +++ /dev/null @@ -1,153 +0,0 @@ -# Maxcio W-UK007S - -# Substitutions -substitutions: - # Device Names - device_name: "test_energy_monitor_plug" - friendly_name: "Test Energy Monitor Plug" - # Icon - main_icon: "power-socket-uk" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "RESTORE_DEFAULT_OFF" - # Activity State Threshold - # Threshold (number) that the device will change from `Idle` to `Active` if power is greater than or equal to - activity_threshold: "10" - -# Basic Config -esphome: - name: "${device_name}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - # Push Button (Toggles Relay When Pressed) - - platform: gpio - pin: - number: GPIO13 - mode: INPUT_PULLUP - inverted: true - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Server Status" - -switch: - # Relay (As Switch) - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO15 - id: relay - restore_mode: "${default_state}" - on_turn_on: - - light.turn_on: - id: led - brightness: 100% - transition_length: 0s - on_turn_off: - - light.turn_off: - id: led - transition_length: 0s - -sensor: - # WiFi Signal Sensor - - platform: wifi_signal - name: "${friendly_name} WiFi Status" - update_interval: 60s - # Power Monitoring - - platform: hlw8012 - sel_pin: - number: GPIO12 - inverted: true - cf_pin: GPIO5 - cf1_pin: GPIO14 - change_mode_every: 3 - update_interval: 3s - voltage: - name: "${friendly_name} Voltage" - id: voltage - unit_of_measurement: V - accuracy_decimals: 1 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.0 - - 602.87506 -> 229.9 - - 609.8 -> 232.8 - power: - name: "${friendly_name} Power" - id: power - unit_of_measurement: W - accuracy_decimals: 0 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 1.14 - - 62.06167 -> 10.93 - - 1503.27161 -> 247.6 - - 1599.81213 -> 263.7 - - 3923.67700 -> 631.4 - - 7109.50928 -> 1148.0 - - 7237.0857 -> 1193.0 - - 7426.71338 -> 1217.0 - current: - name: "${friendly_name} Current" - id: current - unit_of_measurement: A - accuracy_decimals: 3 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.013 - - 0.08208 -> 0.071 - - 1.34223 -> 1.066 - - 5.57170 -> 4.408 - - 6.69184 -> 5.259 - - 6.97187 -> 5.540 - -text_sensor: - # Device Activity State - # e.g. shows as active if power reading above a certain threshold - - platform: template - name: "${friendly_name} Activity State" - icon: "mdi:${main_icon}" - lambda: |- - if (id(power).state >= $activity_threshold) { - return {"Active"}; - } else { - return {"Idle"}; - } - update_interval: 5s - -# Relay State LED -output: - - platform: esp8266_pwm - id: state_led - pin: - number: GPIO0 - inverted: true - -light: - - platform: monochromatic - output: state_led - id: led \ No newline at end of file diff --git a/esphome/tumble_dryer.yaml b/esphome/tumble_dryer.yaml deleted file mode 100644 index fe64dc5..0000000 --- a/esphome/tumble_dryer.yaml +++ /dev/null @@ -1,162 +0,0 @@ -# Maxcio W-UK007S - -# Substitutions -substitutions: - # Device Names - device_name: "tumble_dryer" - friendly_name: "Tumble Dryer" - # Icon - main_icon: "tumble-dryer" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "ALWAYS_ON" - # Activity State Threshold - # Threshold (number) that the device will change from `Idle` to `Active` if power is greater than or equal to - activity_threshold: "10" - -# Basic Config -esphome: - name: "${device_name}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - # Push Button (Toggles Relay When Pressed) - - platform: gpio - pin: - number: GPIO13 - mode: INPUT_PULLUP - inverted: true - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Server Status" - -switch: - # Relay (As Switch) - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO15 - id: relay - restore_mode: "${default_state}" - on_turn_on: - - light.turn_on: - id: led - brightness: 100% - transition_length: 0s - on_turn_off: - - light.turn_off: - id: led - transition_length: 0s - -sensor: - # WiFi Signal Sensor - - platform: wifi_signal - name: "${friendly_name} WiFi Status" - update_interval: 60s - # Power Monitoring - - platform: hlw8012 - sel_pin: - number: GPIO12 - inverted: true - cf_pin: GPIO5 - cf1_pin: GPIO14 - change_mode_every: 3 - update_interval: 3s - voltage: - name: "${friendly_name} Voltage" - id: voltage - unit_of_measurement: V - accuracy_decimals: 1 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.0 - - 602.87506 -> 229.9 - - 609.8 -> 232.8 - power: - name: "${friendly_name} Power" - id: power - unit_of_measurement: W - accuracy_decimals: 0 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 1.14 - - 62.06167 -> 10.93 - - 1503.27161 -> 247.6 - - 1599.81213 -> 263.7 - - 3923.67700 -> 631.4 - - 7109.50928 -> 1148.0 - - 7237.0857 -> 1193.0 - - 7426.71338 -> 1217.0 - current: - name: "${friendly_name} Current" - id: current - unit_of_measurement: A - accuracy_decimals: 3 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.013 - - 0.08208 -> 0.071 - - 1.34223 -> 1.066 - - 5.57170 -> 4.408 - - 6.69184 -> 5.259 - - 6.97187 -> 5.540 - - # Total daily energy sensor - - platform: total_daily_energy - name: "${friendly_name} Daily Energy" - power_id: power - filters: - # Multiplication factor from W to kW is 0.001 - - multiply: 0.001 - unit_of_measurement: kWh - -text_sensor: - # Device Activity State - # e.g. shows as active if power reading above a certain threshold - - platform: template - name: "${friendly_name} Activity State" - icon: "mdi:${main_icon}" - lambda: |- - if (id(power).state >= $activity_threshold) { - return {"Active"}; - } else { - return {"Idle"}; - } - update_interval: 5s - -# Relay State LED -output: - - platform: esp8266_pwm - id: state_led - pin: - number: GPIO0 - inverted: true - -light: - - platform: monochromatic - output: state_led - id: led \ No newline at end of file diff --git a/esphome/upstairs_air_freshener.yaml b/esphome/upstairs_air_freshener.yaml deleted file mode 100644 index f295eed..0000000 --- a/esphome/upstairs_air_freshener.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Sonoff S20 - -# Substitutions -substitutions: - # Device Names - device_name: "upstairs_air_freshener" - friendly_name: "Upstairs Air Freshener" - # Comment / Info To Display In ESPHome Front End - device_comment: "" - # Icon - main_icon: "spray" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "RESTORE_DEFAULT_OFF" - -# Basic Config -esphome: - name: "${device_name}" - comment: "${device_comment}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - - platform: gpio - pin: - number: GPIO0 - mode: INPUT_PULLUP - inverted: True - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Status" - -sensor: - - platform: wifi_signal - name: "${friendly_name} WiFi Signal" - update_interval: 60s - -switch: - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO12 - id: relay - restore_mode: "${default_state}" - -status_led: - pin: - number: GPIO13 \ No newline at end of file diff --git a/esphome/washing_machine.yaml b/esphome/washing_machine.yaml deleted file mode 100644 index 3f771de..0000000 --- a/esphome/washing_machine.yaml +++ /dev/null @@ -1,162 +0,0 @@ -# Maxcio W-UK007S - -# Substitutions -substitutions: - # Device Names - device_name: "washing_machine" - friendly_name: "Washing Machine" - # Icon - main_icon: "washing-machine" - # Default Relay State - # Aka: `restore_mode` in documentation - # Options: `RESTORE_DEFAULT_OFF`, `RESTORE_DEFAULT_ON`, `ALWAYS_ON` & `ALWAYS_OFF` - default_state: "ALWAYS_ON" - # Activity State Threshold - # Threshold (number) that the device will change from `Idle` to `Active` if power is greater than or equal to - activity_threshold: "10" - -# Basic Config -esphome: - name: "${device_name}" - platform: ESP8266 - board: esp01_1m - -wifi: - ssid: !secret esphome_wifi_ssid - password: !secret esphome_wifi_password - -logger: - -api: - reboot_timeout: 15min - -ota: - password: !secret esphome_ota_password - -time: - - platform: sntp - id: sntp_time - -# Device Specific Config -binary_sensor: - # Push Button (Toggles Relay When Pressed) - - platform: gpio - pin: - number: GPIO13 - mode: INPUT_PULLUP - inverted: true - name: "${friendly_name} Button" - on_press: - - switch.toggle: relay - - platform: status - name: "${friendly_name} Server Status" - -switch: - # Relay (As Switch) - - platform: gpio - name: "${friendly_name}" - icon: "mdi:${main_icon}" - pin: GPIO15 - id: relay - restore_mode: "${default_state}" - on_turn_on: - - light.turn_on: - id: led - brightness: 100% - transition_length: 0s - on_turn_off: - - light.turn_off: - id: led - transition_length: 0s - -sensor: - # WiFi Signal Sensor - - platform: wifi_signal - name: "${friendly_name} WiFi Status" - update_interval: 60s - # Power Monitoring - - platform: hlw8012 - sel_pin: - number: GPIO12 - inverted: true - cf_pin: GPIO5 - cf1_pin: GPIO14 - change_mode_every: 3 - update_interval: 3s - voltage: - name: "${friendly_name} Voltage" - id: voltage - unit_of_measurement: V - accuracy_decimals: 1 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.0 - - 602.87506 -> 229.9 - - 609.8 -> 232.8 - power: - name: "${friendly_name} Power" - id: power - unit_of_measurement: W - accuracy_decimals: 0 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 1.14 - - 62.06167 -> 10.93 - - 1503.27161 -> 247.6 - - 1599.81213 -> 263.7 - - 3923.67700 -> 631.4 - - 7109.50928 -> 1148.0 - - 7237.0857 -> 1193.0 - - 7426.71338 -> 1217.0 - current: - name: "${friendly_name} Current" - id: current - unit_of_measurement: A - accuracy_decimals: 3 - filters: - # Map from sensor -> measured value - - calibrate_linear: - - 0.0 -> 0.013 - - 0.08208 -> 0.071 - - 1.34223 -> 1.066 - - 5.57170 -> 4.408 - - 6.69184 -> 5.259 - - 6.97187 -> 5.540 - - # Total daily energy sensor - - platform: total_daily_energy - name: "${friendly_name} Daily Energy" - power_id: power - filters: - # Multiplication factor from W to kW is 0.001 - - multiply: 0.001 - unit_of_measurement: kWh - -text_sensor: - # Device Activity State - # e.g. shows as active if power reading above a certain threshold - - platform: template - name: "${friendly_name} Activity State" - icon: "mdi:${main_icon}" - lambda: |- - if (id(power).state >= $activity_threshold) { - return {"Active"}; - } else { - return {"Idle"}; - } - update_interval: 5s - -# Relay State LED -output: - - platform: esp8266_pwm - id: state_led - pin: - number: GPIO0 - inverted: true - -light: - - platform: monochromatic - output: state_led - id: led \ No newline at end of file diff --git a/integrations/binary_sensor.yaml b/integrations/binary_sensors.yaml similarity index 100% rename from integrations/binary_sensor.yaml rename to integrations/binary_sensors.yaml diff --git a/integrations/camera.yaml b/integrations/camera.yaml deleted file mode 100644 index 406ee34..0000000 --- a/integrations/camera.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Cameras -# https://www.home-assistant.io/integrations/camera/ - -camera: !include_dir_list ../entities/cameras diff --git a/integrations/cloud.yaml b/integrations/cloud.yaml deleted file mode 100644 index a91a4bf..0000000 --- a/integrations/cloud.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -# Cloud -# https://www.home-assistant.io/integrations/cloud/ - -cloud: - # Customize the Alexa integration with the Nabu Casa Cloud - alexa: - # Entity specific configuration for Google Assistant. - # These are loaded from a separate directory. - entity_config: !include_dir_merge_named ../assistants/alexa - # filter: - # # Filters for entities to include/exclude from Alexa. - # include_entities: [] - # exclude_entities: [] diff --git a/integrations/config.yaml b/integrations/config.yaml deleted file mode 100644 index 15138fe..0000000 --- a/integrations/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Config -# https://www.home-assistant.io/integrations/config/ - -config: diff --git a/integrations/default_config.yaml b/integrations/default_config.yaml new file mode 100644 index 0000000..7190a28 --- /dev/null +++ b/integrations/default_config.yaml @@ -0,0 +1,5 @@ +--- +# Default Config +# https://www.home-assistant.io/integrations/default_config/ + +default_config: ~ \ No newline at end of file diff --git a/integrations/device_tracker.yaml b/integrations/device_tracker.yaml deleted file mode 100644 index 5b85478..0000000 --- a/integrations/device_tracker.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Device Trackers -# https://www.home-assistant.io/components/device_tracker/ - -device_tracker: !include_dir_list ../entities/device_trackers diff --git a/integrations/discovery.yaml b/integrations/discovery.yaml deleted file mode 100644 index 5a382cf..0000000 --- a/integrations/discovery.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Discovery -# https://www.home-assistant.io/components/discovery/ - -discovery: diff --git a/integrations/evohome.yaml b/integrations/evohome.yaml deleted file mode 100644 index 7a24b24..0000000 --- a/integrations/evohome.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Honeywell Evohome -# https://www.home-assistant.io/integrations/evohome/ - -evohome: - username: !secret evohome_username - password: !secret evohome_password - location_idx: 1 - scan_interval: 180 diff --git a/integrations/frontend.yaml b/integrations/frontend.yaml index 0105ab9..657784e 100644 --- a/integrations/frontend.yaml +++ b/integrations/frontend.yaml @@ -3,14 +3,4 @@ # https://www.home-assistant.io/integrations/frontend/ frontend: - # Version of the JavaScript to serve to clients. - javascript_version: auto - - # List of additional resources to load (latest javascript) - extra_html_url: [] - - # List of additional resource to load (es5 javascript) - extra_html_url_es5: [] - - # We can define additional/custom themes for the frontend here. themes: !include_dir_merge_named ../themes diff --git a/integrations/garbage_collection.yaml b/integrations/garbage_collection.yaml deleted file mode 100644 index a18496e..0000000 --- a/integrations/garbage_collection.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Garbage Collection -# Managed through HACS -# https://github.com/bruxy70/Garbage-Collection - -garbage_collection: - sensors: - - name: Black Bin - collection_days: "thu" - frequency: "every-n-days" - period: 14 - first_date: "2020-01-02" - exclude_dates: [] - - name: Red Bin - collection_days: "thu" - frequency: "every-n-days" - period: 14 - first_date: "2020-01-09" - exclude_dates: [] - - name: Green Bin - collection_days: "thu" - frequency: "every-n-days" - period: 14 - first_date: "2020-01-09" - exclude_dates: - - "2020-01-23" - - "2020-02-06" - - "2020-02-20" \ No newline at end of file diff --git a/integrations/group.yaml b/integrations/group.yaml index f063e84..00250df 100644 --- a/integrations/group.yaml +++ b/integrations/group.yaml @@ -1,5 +1,5 @@ --- -# Groups -# https://www.home-assistant.io/components/group/ +# Group +# https://www.home-assistant.io/integrations/group/ group: !include_dir_merge_named ../entities/groups diff --git a/integrations/hassio.yaml b/integrations/hassio.yaml index 5865193..db8a2b0 100644 --- a/integrations/hassio.yaml +++ b/integrations/hassio.yaml @@ -1,6 +1,4 @@ --- # Hassio -# Enables the hassio pannel on the frontend -# -hassio: +hassio: ~ diff --git a/integrations/history.yaml b/integrations/history.yaml deleted file mode 100644 index bcf2025..0000000 --- a/integrations/history.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# History -# https://www.home-assistant.io/integrations/history/ - -history: diff --git a/integrations/homeassistant.yaml b/integrations/homeassistant.yaml index d72255f..eadb1b4 100644 --- a/integrations/homeassistant.yaml +++ b/integrations/homeassistant.yaml @@ -1,23 +1,7 @@ --- -# Home Assistant component -# # https://www.home-assistant.io/docs/configuration/basic/ +# Home Assistant +# homeassistant: - # Home Assistant Instance Name - name: Connock-Adams Home - - # Units - unit_system: metric - temperature_unit: C - - # Timezone - time_zone: Europe/London - - # Customize individual entities customize: !include_dir_merge_named ../customizations/entities - - # Custimize all entities in a domain - customize_domain: !include_dir_merge_named ../customizations/domains - - # Custimize entities based on a matching pattern - customize_glob: !include_dir_merge_named ../customizations/globs + customize_domain: !include_dir_merge_named ../customizations/domains \ No newline at end of file diff --git a/integrations/http.yaml b/integrations/http.yaml deleted file mode 100644 index b1bbf0b..0000000 --- a/integrations/http.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -# HTTP -# https://home-assistant.io/components/http/ - -http: - # URL Home Assistant is avaliable on - base_url: !secret http_base_url - - # Server Port (Default: 8123) - server_port: !secret http_server_port - - # List of origin domain names to allow CORS requests from. - cors_allowed_origins: !secret http_cors_allowed_origins - - # Enable parsing of the X-Forwarded-For header, passing on the client’s - # correct IP address. - use_x_forwarded_for: true - - # List of trusted proxies, consisting of IP addresses or networks, - # that are allowed to set the X-Forwarded-For header. - trusted_proxies: !secret http_trusted_proxies - - # IP Banning - ip_ban_enabled: false - - # Number of failed login attempts from an IP Address before becoming banned - login_attempts_threshold: 5 diff --git a/integrations/hue.yaml b/integrations/hue.yaml deleted file mode 100644 index d210153..0000000 --- a/integrations/hue.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -# Philips Hue -# https://www.home-assistant.io/integrations/hue/ - -hue: - bridges: - - host: !secret phillips_hue_hub_ip - allow_unreachable: true - allow_hue_groups: true diff --git a/integrations/influxdb.yaml.disable b/integrations/influxdb.yaml.disable deleted file mode 100644 index 0b6a7c2..0000000 --- a/integrations/influxdb.yaml.disable +++ /dev/null @@ -1,11 +0,0 @@ -# InfluxDB -# https://www.home-assistant.io/integrations/influxdb/ - -influxdb: - host: a0d7b954_influxdb - port: 8086 - database: homeassistant - username: homeassistant - password: 123456789 - max_retries: 3 - default_measurement: state \ No newline at end of file diff --git a/integrations/input_boolean.yaml b/integrations/input_boolean.yaml deleted file mode 100644 index 59b4cef..0000000 --- a/integrations/input_boolean.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Input Booleans -# https://www.home-assistant.io/integrations/input_boolean/ - -input_boolean: !include_dir_merge_named ../entities/input_booleans diff --git a/integrations/input_datetime.yaml b/integrations/input_datetime.yaml deleted file mode 100644 index a0cdf22..0000000 --- a/integrations/input_datetime.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Input Datetime -# https://www.home-assistant.io/integrations/input_datetime/ - -input_datetime: !include_dir_merge_named ../entities/input_datetime diff --git a/integrations/input_number.yaml b/integrations/input_number.yaml deleted file mode 100644 index c8a3147..0000000 --- a/integrations/input_number.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Input Number -# https://www.home-assistant.io/integrations/input_number/ - -input_number: !include_dir_merge_named ../entities/input_numbers diff --git a/integrations/input_select.yaml b/integrations/input_select.yaml deleted file mode 100644 index 7b16bcb..0000000 --- a/integrations/input_select.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Input Select -# https://www.home-assistant.io/integrations/input_select/ - -input_select: !include_dir_merge_named ../entities/input_selects diff --git a/integrations/input_text.yaml b/integrations/input_text.yaml deleted file mode 100644 index 43f7264..0000000 --- a/integrations/input_text.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Input Text -# https://www.home-assistant.io/integrations/input_texts/ - -input_text: !include_dir_merge_named ../entities/input_texts diff --git a/integrations/ios.yaml b/integrations/ios.yaml index fb9bdc7..6739ba5 100644 --- a/integrations/ios.yaml +++ b/integrations/ios.yaml @@ -1,6 +1,5 @@ --- # iOS -# https://www.home-assistant.io/integrations/ios/ -# https://www.home-assistant.io/docs/ecosystem/ios/ +# https://companion.home-assistant.io/docs/getting_started/getting-started -ios: +ios: ~ \ No newline at end of file diff --git a/integrations/light.yaml b/integrations/light.yaml index 2f28188..3719cfb 100644 --- a/integrations/light.yaml +++ b/integrations/light.yaml @@ -1,5 +1,5 @@ --- -# Lights +# Light # https://www.home-assistant.io/integrations/light/ light: !include_dir_list ../entities/lights diff --git a/integrations/logbook.yaml b/integrations/logbook.yaml deleted file mode 100644 index cd63480..0000000 --- a/integrations/logbook.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Logbook -# https://www.home-assistant.io/integrations/logbook/ - -logbook: diff --git a/integrations/logger.yaml.disable b/integrations/logger.yaml.disable deleted file mode 100644 index 96af62c..0000000 --- a/integrations/logger.yaml.disable +++ /dev/null @@ -1,8 +0,0 @@ -# Logger -# https://www.home-assistant.io/integrations/logger/ - -logger: - default: info - logs: - pydeconz: debug - homeassistant.components.deconz: debug \ No newline at end of file diff --git a/integrations/lovelace.yaml b/integrations/lovelace.yaml deleted file mode 100644 index 691b56b..0000000 --- a/integrations/lovelace.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Lovelace component for confuring the UI. -# https://www.home-assistant.io/lovelace/ - -lovelace: diff --git a/integrations/map.yaml b/integrations/map.yaml deleted file mode 100644 index 05ed069..0000000 --- a/integrations/map.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Map -# https://www.home-assistant.io/integrations/map/ - -map: diff --git a/integrations/mobile_app.yaml b/integrations/mobile_app.yaml deleted file mode 100644 index 1c9753b..0000000 --- a/integrations/mobile_app.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Mobile App -# https://www.home-assistant.io/integrations/mobile_app/ - -mobile_app: diff --git a/integrations/notify.yaml b/integrations/notify.yaml index aaf1830..0d8f4cb 100644 --- a/integrations/notify.yaml +++ b/integrations/notify.yaml @@ -1,6 +1,5 @@ --- # Notify -# https://www.home-assistant.io/components/notify/ -# https://www.home-assistant.io/components/notify.group/ +# https://www.home-assistant.io/integrations/notify/ -notify: !include_dir_list ../entities/notify +notify: !include_dir_list ../entities/notifiers diff --git a/integrations/person.yaml b/integrations/person.yaml deleted file mode 100644 index 25ec13a..0000000 --- a/integrations/person.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Person -# https://www.home-assistant.io/integrations/person/ - -person: !include_dir_list ../entities/persons diff --git a/integrations/recorder.yaml b/integrations/recorder.yaml index 0feefee..b175339 100644 --- a/integrations/recorder.yaml +++ b/integrations/recorder.yaml @@ -3,8 +3,9 @@ # https://www.home-assistant.io/integrations/recorder/ recorder: - # The number of days of history to keep in the recorder database - purge_keep_days: 7 - - # How often (in days) the purge task runs - purge_interval: 1 + # DB is stored using MariaDB + db_url: !secret recorder_db_url + purge_keep_days: 3 + + # Purging is managed using an automation + auto_purge: false \ No newline at end of file diff --git a/integrations/ring.yaml b/integrations/ring.yaml deleted file mode 100644 index c86775a..0000000 --- a/integrations/ring.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Ring -# https://www.home-assistant.io/integrations/ring/ - -# DISABLED: Currently disabled due to error -# https://community.home-assistant.io/t/ring-integration-setup-fails/156240/250 - -ring: - username: !secret ring_username - password: !secret ring_password diff --git a/integrations/scene.yaml b/integrations/scene.yaml index 70758e3..88f5473 100644 --- a/integrations/scene.yaml +++ b/integrations/scene.yaml @@ -2,6 +2,5 @@ # Scenes # https://www.home-assistant.io/integrations/scene -# This include is for the scene editor/UI scene: !include ../scenes.yaml scene split: !include_dir_list ../scenes diff --git a/integrations/script.yaml b/integrations/script.yaml index 7646847..c7c0967 100644 --- a/integrations/script.yaml +++ b/integrations/script.yaml @@ -1,5 +1,5 @@ --- -# Scripts +# Script # https://www.home-assistant.io/integrations/script/ -script: !include_dir_merge_named ../scripts \ No newline at end of file +script: ~ diff --git a/integrations/sun.yaml b/integrations/sun.yaml deleted file mode 100644 index 0f6051e..0000000 --- a/integrations/sun.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Sun -# https://www.home-assistant.io/integrations/sun/ - -sun: - # The (physical) elevation of your location, in meters above sea level. - elevation: !secret home_elevation diff --git a/integrations/switch.yaml b/integrations/switch.yaml deleted file mode 100644 index 645fb07..0000000 --- a/integrations/switch.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Switches -# https://www.home-assistant.io/integrations/switch/ - -switch: !include_dir_list ../entities/switches diff --git a/integrations/system_health.yaml b/integrations/system_health.yaml deleted file mode 100644 index f9e7448..0000000 --- a/integrations/system_health.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# System Health -# https://www.home-assistant.io/integrations/system_health/ - -system_health: diff --git a/integrations/tradfri.yaml.disable b/integrations/tradfri.yaml.disable deleted file mode 100644 index b6646a2..0000000 --- a/integrations/tradfri.yaml.disable +++ /dev/null @@ -1,8 +0,0 @@ -# Tradfri -# https://www.home-assistant.io/integrations/tradfri/ - -# DISABLED: No longer required as replaced by ConBee II - -tradfri: - host: 10.1.1.174 - allow_tradfri_groups: true diff --git a/integrations/twilio.yaml b/integrations/twilio.yaml deleted file mode 100644 index 8d80a2d..0000000 --- a/integrations/twilio.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Twilio -# https://www.home-assistant.io/components/twilio/ -# https://www.home-assistant.io/components/twilio_sms/ - -twilio: - account_sid: !secret twilio_sid - auth_token: !secret twilio_auth_token diff --git a/integrations/updater.yaml b/integrations/updater.yaml deleted file mode 100644 index c5b2b4d..0000000 --- a/integrations/updater.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -# Updater -# https://www.home-assistant.io/integrations/updater/ -# -# For more information, please see: -# https://home-assistant.io/blog/2016/10/25/explaining-the-updater/ - -updater: - # Share information with Home Assistant Developers - reporting: true - - # Report list of components to Home Assistant Developers - include_used_components: true diff --git a/integrations/weather.yaml b/integrations/weather.yaml deleted file mode 100644 index ef32328..0000000 --- a/integrations/weather.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Weather -# https://www.home-assistant.io/integrations/weather/ - -weather: !include_dir_list ../entities/weather diff --git a/integrations/zone.yaml b/integrations/zone.yaml deleted file mode 100644 index ef4b3f7..0000000 --- a/integrations/zone.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# Zones -# https://www.home-assistant.io/integrations/zone/ - -zone: !include_dir_list ../entities/zones diff --git a/scenes.yaml b/scenes.yaml index f9182ee..e69de29 100644 --- a/scenes.yaml +++ b/scenes.yaml @@ -1,278 +0,0 @@ -- id: '1578761854442' - name: Colourful Mode (Living Room) - entities: - light.living_room_left_color: - brightness: 254 - effect: none - effect_list: - - colorloop - - random - friendly_name: Left Colour - hs_color: - - 180.615 - - 76.471 - icon: mdi:track-light - rgb_color: - - 59 - - 253 - - 255 - state: 'on' - supported_features: 61 - xy_color: - - 0.162 - - 0.339 - light.living_room_left_lamp: - brightness: 254 - friendly_name: Living Room Left Lamp - hs_color: - - 299.997 - - 100 - rgb_color: - - 254 - - 0 - - 255 - state: 'on' - supported_features: 49 - xy_color: - - 0.384 - - 0.154 - light.living_room_right_color: - brightness: 254 - effect: none - effect_list: - - colorloop - - random - friendly_name: Right Colour - hs_color: - - 100.706 - - 100 - icon: mdi:track-light - rgb_color: - - 81 - - 255 - - 0 - state: 'on' - supported_features: 61 - xy_color: - - 0.215 - - 0.711 - light.living_room_right_lamp: - brightness: 254 - friendly_name: Living Room Right Lamp - hs_color: - - 29.999 - - 100 - rgb_color: - - 255 - - 127 - - 0 - state: 'on' - supported_features: 49 - xy_color: - - 0.612 - - 0.374 - light.living_room_tall_lamp: - brightness: 254 - friendly_name: Living Room Tall Lamp - hs_color: - - 269.999 - - 100 - rgb_color: - - 127 - - 0 - - 255 - state: 'on' - supported_features: 49 - xy_color: - - 0.217 - - 0.077 -- id: '1578762173280' - name: Normal Mode (Living Room) - entities: - light.living_room_left_color: - effect_list: - - colorloop - - random - friendly_name: Left Colour - icon: mdi:track-light - state: 'off' - supported_features: 61 - light.living_room_left_lamp: - brightness: 254 - friendly_name: Living Room Left Lamp - hs_color: - - 29.812 - - 65.252 - rgb_color: - - 255 - - 171 - - 88 - state: 'on' - supported_features: 49 - xy_color: - - 0.52 - - 0.392 - light.living_room_right_color: - effect_list: - - colorloop - - random - friendly_name: Right Colour - icon: mdi:track-light - state: 'off' - supported_features: 61 - light.living_room_right_lamp: - brightness: 254 - friendly_name: Living Room Right Lamp - hs_color: - - 29.812 - - 65.252 - rgb_color: - - 255 - - 171 - - 88 - state: 'on' - supported_features: 49 - xy_color: - - 0.52 - - 0.392 - light.living_room_tall_lamp: - brightness: 254 - friendly_name: Living Room Tall Lamp - hs_color: - - 29.812 - - 65.252 - rgb_color: - - 255 - - 171 - - 88 - state: 'on' - supported_features: 49 - xy_color: - - 0.52 - - 0.392 -- id: '1578762351233' - name: Movie Mode (Living Room) - entities: - light.living_room_left_color: - effect_list: - - colorloop - - random - friendly_name: Left Colour - icon: mdi:track-light - state: 'off' - supported_features: 61 - light.living_room_left_lamp: - brightness: 40 - friendly_name: Living Room Left Lamp - hs_color: - - 29.999 - - 57.142 - rgb_color: - - 255 - - 182 - - 109 - state: 'on' - supported_features: 49 - xy_color: - - 0.491 - - 0.39 - light.living_room_right_color: - effect_list: - - colorloop - - random - friendly_name: Right Colour - icon: mdi:track-light - state: 'off' - supported_features: 61 - light.living_room_right_lamp: - brightness: 40 - friendly_name: Living Room Right Lamp - hs_color: - - 29.999 - - 57.142 - rgb_color: - - 255 - - 182 - - 109 - state: 'on' - supported_features: 49 - xy_color: - - 0.491 - - 0.39 - light.living_room_tall_lamp: - brightness: 40 - friendly_name: Living Room Tall Lamp - hs_color: - - 29.999 - - 57.142 - rgb_color: - - 255 - - 182 - - 109 - state: 'on' - supported_features: 49 - xy_color: - - 0.491 - - 0.39 -- id: '1578785060571' - name: Daylight Mode (Living Room) - entities: - light.living_room_left_color: - effect_list: - - colorloop - - random - friendly_name: Living Room Left Colour - state: 'off' - supported_features: 61 - light.living_room_left_lamp: - brightness: 254 - friendly_name: Living Room Left Lamp - hs_color: - - 31.861 - - 37.369 - rgb_color: - - 255 - - 210 - - 159 - state: 'on' - supported_features: 49 - xy_color: - - 0.423 - - 0.377 - light.living_room_right_color: - effect_list: - - colorloop - - random - friendly_name: Living Room Right Colour - state: 'off' - supported_features: 61 - light.living_room_right_lamp: - brightness: 254 - friendly_name: Living Room Right Lamp - hs_color: - - 31.861 - - 37.369 - rgb_color: - - 255 - - 210 - - 159 - state: 'on' - supported_features: 49 - xy_color: - - 0.423 - - 0.377 - light.living_room_tall_lamp: - brightness: 254 - friendly_name: Living Room Tall Lamp - hs_color: - - 31.861 - - 37.369 - rgb_color: - - 255 - - 210 - - 159 - state: 'on' - supported_features: 49 - xy_color: - - 0.423 - - 0.377 diff --git a/secrets_dummy.yaml b/secrets_dummy.yaml deleted file mode 100644 index 7180cfb..0000000 --- a/secrets_dummy.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Secrets File - -# Base URL -http_base_url: https://google.com - -# HTTP Server Port -http_server_port: 8123 - -# HTTP Trusted Proxies -http_trusted_proxies: ["0.0.0.0"] - -# HTTP Cors Allowed Origins -http_cors_allowed_origins: ["0.0.0.0"] - -# Home Location Information -home_latitude: 00.00 -home_longitude: 00.00 -home_elevation: 00.00 - -# Zone - Jon's Parents -jon_parents_latitude: 00.00 -jon_parents_longitude: 00.00 - -# Zone - Lee's Parents -lee_parents_latitude: 00.00 -lee_parents_longitude: 00.00 - -# Zone - Jon's Work (Hub) -jon_work_hub_latitude: 00.00 -jon_work_hub_longitude: 00.00 - -# Zone - Jon's Work (Hospital) -jon_work_hospital_latitude: 00.00 -jon_work_hospital_longitude: 00.00 - -# Zone - Lee's Work -lee_work_latitude: 00.00 -lee_work_longitude: 00.00 - -# MQTT -MQTT_username: username -MQTT_password: password - -# HIVE -hive_username: username -hive_password: password - -# TravisCI API Key -travisci_api_key: xyz - -# Device IP Addresses -phillips_hue_hub_ip: 0.0.0.0 -living_room_tv_ip: 0.0.0.0 -living_room_amp_ip: 0.0.0.0 - -# Node-Red -nodered_credential_secret: password - -# Twilio -twilio_sid: sid -twilio_auth_token: token - -# Amazon Alexa -amazon_alexa_username: username -amazon_alexa_password: password - -# Ring -ring_username: username -ring_password: password - -# Evohome -evohome_username: username -evohome_password: password - -# Darksky -darksky_api_key: xxxxx diff --git a/www/community/button-card/button-card.js b/www/community/button-card/button-card.js new file mode 100644 index 0000000..6f85563 --- /dev/null +++ b/www/community/button-card/button-card.js @@ -0,0 +1,547 @@ +function t(t,e,i,n){var r,s=arguments.length,a=s<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,i,n);else for(var o=t.length-1;o>=0;o--)(r=t[o])&&(a=(s<3?r(a):s>3?r(e,i,a):r(e,i))||a);return s>3&&a&&Object.defineProperty(e,i,a),a}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}},n=`{{lit-${String(Math.random()).slice(2)}}}`,r=`\x3c!--${n}--\x3e`,s=new RegExp(`${n}|${r}`);class a{constructor(t,e){this.parts=[],this.element=e;const i=[],r=[],a=document.createTreeWalker(e.content,133,null,!1);let l=0,u=-1,d=0;const{strings:p,values:{length:f}}=t;for(;d0;){const e=p[d],i=h.exec(e)[2],n=i.toLowerCase()+"$lit$",r=t.getAttribute(n);t.removeAttribute(n);const a=r.split(s);this.parts.push({type:"attribute",index:u,name:i,strings:a}),d+=a.length-1}}"TEMPLATE"===t.tagName&&(r.push(t),a.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(n)>=0){const n=t.parentNode,r=e.split(s),a=r.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(""),h=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function u(t,e){const{element:{content:i},parts:n}=t,r=document.createTreeWalker(i,133,null,!1);let s=p(n),a=n[s],o=-1,l=0;const c=[];let h=null;for(;r.nextNode();){o++;const t=r.currentNode;for(t.previousSibling===h&&(h=null),e.has(t)&&(c.push(t),null===h&&(h=t)),null!==h&&l++;void 0!==a&&a.index===o;)a.index=null!==h?-1:a.index-l,s=p(n,s),a=n[s]}c.forEach((t=>t.parentNode.removeChild(t)))}const d=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(...e)=>{const i=t(...e);return f.set(i,!0),i},g=t=>"function"==typeof t&&f.has(t),_={},b={};class y{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 i of this.__parts)void 0!==i&&i.commit()}_clone(){const t=e?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),i=[],n=this.template.parts,r=document.createTreeWalker(t,133,null,!1);let s,a=0,o=0,c=r.nextNode();for(;at}),w=` ${n} `;class S{constructor(t,e,i,n){this.strings=t,this.values=e,this.type=i,this.processor=n}getHTML(){const t=this.strings.length-1;let e="",i=!1;for(let s=0;s-1||i)&&-1===t.indexOf("--\x3e",a+1);const o=h.exec(t);e+=null===o?t+(i?w:r):t.substr(0,o.index)+o[1]+o[2]+"$lit$"+o[3]+n}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");let e=this.getHTML();return void 0!==v&&(e=v.createHTML(e)),t.innerHTML=e,t}}const x=t=>null===t||!("object"==typeof t||"function"==typeof t),k=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 n=0;n{try{const t={get capture(){return A=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class N{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),n=null!=t&&(null==e||i);i&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),n&&(this.__options=j(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 j=t=>t&&(A?{capture:t.capture,passive:t.passive,once:t.once}:t.capture);function $(t){let e=H.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},H.set(t.type,e));let i=e.stringsArray.get(t.strings);if(void 0!==i)return i;const r=t.strings.join(n);return i=e.keyString.get(r),void 0===i&&(i=new a(t,t.getTemplateElement()),e.keyString.set(r,i)),e.stringsArray.set(t.strings,i),i}const H=new Map,R=new WeakMap;const V=new class{handleAttributeExpressions(t,e,i,n){const r=e[0];if("."===r){return new E(t,e.slice(1),i).parts}if("@"===r)return[new N(t,e.slice(1),n.eventContext)];if("?"===r)return[new C(t,e.slice(1),i)];return new O(t,e,i).parts}handleTextExpression(t){return new M(t)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.3.0");const D=(t,...e)=>new S(t,e,"html",V),L=(t,e)=>`${t}--${e}`;let F=!0;void 0===window.ShadyCSS?F=!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."),F=!1);const I=t=>e=>{const i=L(e.type,t);let r=H.get(i);void 0===r&&(r={stringsArray:new WeakMap,keyString:new Map},H.set(i,r));let s=r.stringsArray.get(e.strings);if(void 0!==s)return s;const o=e.strings.join(n);if(s=r.keyString.get(o),void 0===s){const i=e.getTemplateElement();F&&window.ShadyCSS.prepareTemplateDom(i,t),s=new a(e,i),r.keyString.set(o,s)}return r.stringsArray.set(e.strings,s),s},z=["html","svg"],U=new Set,q=(t,e,i)=>{U.add(t);const n=i?i.element:document.createElement("template"),r=e.querySelectorAll("style"),{length:s}=r;if(0===s)return void window.ShadyCSS.prepareTemplateStyles(n,t);const a=document.createElement("style");for(let c=0;c{z.forEach((e=>{const i=H.get(L(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)})),u(t,i)}))}))})(t);const o=n.content;i?function(t,e,i=null){const{element:{content:n},parts:r}=t;if(null==i)return void n.appendChild(e);const s=document.createTreeWalker(n,133,null,!1);let a=p(r),o=0,l=-1;for(;s.nextNode();)for(l++,s.currentNode===i&&(o=d(e),i.parentNode.insertBefore(e,i));-1!==a&&r[a].index===l;){if(o>0){for(;-1!==a;)r[a].index+=o,a=p(r,a);return}a=p(r,a)}}(i,a,o.firstChild):o.insertBefore(a,o.firstChild),window.ShadyCSS.prepareTemplateStyles(n,t);const l=o.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==l)e.insertBefore(l.cloneNode(!0),e.firstChild);else if(i){o.insertBefore(a,o.firstChild);const t=new Set;t.add(a),u(i,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const Y={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}},B=(t,e)=>e!==t&&(e==e||t==t),W={attribute:!0,type:String,converter:Y,reflect:!1,hasChanged:B};class G extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach(((e,i)=>{const n=this._attributeNameForProperty(i,e);void 0!==n&&(this._attributeToPropertyMap.set(n,i),t.push(n))})),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=W){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const i="symbol"==typeof t?Symbol():"__"+t,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const r=this[t];this[e]=n,this.requestUpdateInternal(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||W}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=B){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,n=e.converter||Y,r="function"==typeof n?n:n.fromAttribute;return r?r(t,i):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const i=e.type,n=e.converter;return(n&&n.toAttribute||Y.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=W){const n=this.constructor,r=n._attributeNameForProperty(t,i);if(void 0!==r){const t=n._propertyValueToAttribute(e,i);if(void 0===t)return;this._updateState=8|this._updateState,null==t?this.removeAttribute(r):this.setAttribute(r,t),this._updateState=-9&this._updateState}}_attributeToProperty(t,e){if(8&this._updateState)return;const i=this.constructor,n=i._attributeToPropertyMap.get(t);if(void 0!==n){const t=i.getPropertyOptions(n);this._updateState=16|this._updateState,this[n]=i._propertyValueFromAttribute(e,t),this._updateState=-17&this._updateState}}requestUpdateInternal(t,e,i){let n=!0;if(void 0!==t){const r=this.constructor;i=i||r.getPropertyOptions(t),r._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))):n=!1}!this._hasRequestedUpdate&&n&&(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(e){}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(i){throw t=!1,this._markUpdated(),i}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){}}G.finalized=!0;const Z=(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 J(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):Z(t,e)}const X=(t,e,i)=>{Object.defineProperty(e,i,t)},K=(t,e)=>({kind:"method",placement:"prototype",key:e.key,descriptor:t});const Q=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&&(Q?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");const it={};class nt extends G{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),n=[];i.forEach((t=>n.unshift(t))),this._styles=n}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!Q){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?Q?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!==it&&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 it}}nt.finalized=!0,nt.render=(t,e,n)=>{if(!n||"object"!=typeof n||!n.scopeName)throw new Error("The `scopeName` option is required.");const r=n.scopeName,s=R.has(e),a=F&&11===e.nodeType&&!!e.host,o=a&&!U.has(r),l=o?document.createDocumentFragment():e;if(((t,e,n)=>{let r=R.get(e);void 0===r&&(i(e,e.firstChild),R.set(e,r=new M(Object.assign({templateFactory:$},n))),r.appendInto(e)),r.setValue(t),r.commit()})(t,l,Object.assign({templateFactory:I(r)},n)),o){const t=R.get(l);R.delete(l);const n=t.value instanceof y?t.value.template:void 0;q(r,l,n),i(e,e.firstChild),e.appendChild(l),R.set(e,t)}!s&&a&&window.ShadyCSS.styleElement(e.host)};class rt{constructor(t){this.startPress=e=>{t().then((t=>{t&&t.startPress(e)}))},this.endPress=()=>{t().then((t=>{t&&t.endPress()}))},this.startFocus=()=>{t().then((t=>{t&&t.startFocus()}))},this.endFocus=()=>{t().then((t=>{t&&t.endFocus()}))},this.startHover=()=>{t().then((t=>{t&&t.startHover()}))},this.endHover=()=>{t().then((t=>{t&&t.endHover()}))}}}const st=new WeakMap,at=m((t=>e=>{if(!(e instanceof T)||e instanceof P||"style"!==e.committer.name||e.committer.parts.length>1)throw new Error("The `styleMap` directive must be used in the style attribute and must be the only part in the attribute.");const{committer:i}=e,{style:n}=i.element;let r=st.get(e);void 0===r&&(n.cssText=i.strings.join(" "),st.set(e,r=new Set)),r.forEach((e=>{e in t||(r.delete(e),-1===e.indexOf("-")?n[e]=null:n.removeProperty(e))}));for(const s in t)r.add(s),-1===s.indexOf("-")?n[s]=t[s]:n.setProperty(s,t[s])})),ot=new WeakMap,lt=m((t=>e=>{if(!(e instanceof M))throw new Error("unsafeHTML can only be used in text bindings");const i=ot.get(e);if(void 0!==i&&x(t)&&t===i.value&&e.value===i.fragment)return;const n=document.createElement("template");n.innerHTML=t;const r=document.importNode(n.content,!0);e.setValue(r),ot.set(e,{value:t,fragment:r})}));class ct{constructor(t){this.classes=new Set,this.changed=!1,this.element=t;const e=(t.getAttribute("class")||"").split(/\s+/);for(const i of e)this.classes.add(i)}add(t){this.classes.add(t),this.changed=!0}remove(t){this.classes.delete(t),this.changed=!0}commit(){if(this.changed){let t="";this.classes.forEach((e=>t+=e+" ")),this.element.setAttribute("class",t)}}}const ht=new WeakMap,ut=m((t=>e=>{if(!(e instanceof T)||e instanceof P||"class"!==e.committer.name||e.committer.parts.length>1)throw new Error("The `classMap` directive must be used in the `class` attribute and must be the only part in the attribute.");const{committer:i}=e,{element:n}=i;let r=ht.get(e);void 0===r&&(n.setAttribute("class",i.strings.join(" ")),ht.set(e,r=new Set));const s=n.classList||new ct(n);r.forEach((e=>{e in t||(s.remove(e),r.delete(e))}));for(const a in t){const e=t[a];e!=r.has(a)&&(e?(s.add(a),r.add(a)):(s.remove(a),r.delete(a)))}"function"==typeof s.commit&&s.commit()}));var dt=/d{1,4}|M{1,4}|YY(?:YY)?|S{1,3}|Do|ZZ|Z|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g,pt="[^\\s]+",ft=/\[([^]*?)\]/gm;function mt(t,e){for(var i=[],n=0,r=t.length;n-1?n:null}};function _t(t){for(var e=[],i=1;i3?0:(t-t%10!=10?1:0)*t%10]}},St=_t({},wt),xt=function(t,e){for(void 0===e&&(e=2),t=String(t);t.length0?"-":"+")+xt(100*Math.floor(Math.abs(e)/60)+Math.abs(e)%60,4)},Z:function(t){var e=t.getTimezoneOffset();return(e>0?"-":"+")+xt(Math.floor(Math.abs(e)/60),2)+":"+xt(Math.abs(e)%60,2)}},Ot=function(t){return+t-1},Tt=[null,"[1-9]\\d?"],Mt=[null,pt],Ct=["isPm",pt,function(t,e){var i=t.toLowerCase();return i===e.amPm[0]?0:i===e.amPm[1]?1:null}],Et=["timezoneOffset","[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z?",function(t){var e=(t+"").match(/([+-]|\d\d)/gi);if(e){var i=60*+e[1]+parseInt(e[2],10);return"+"===e[0]?i:-i}return 0}],Pt=(gt("monthNamesShort"),gt("monthNames"),{default:"ddd MMM DD YYYY HH:mm:ss",shortDate:"M/D/YY",mediumDate:"MMM D, YYYY",longDate:"MMMM D, YYYY",fullDate:"dddd, MMMM D, YYYY",isoDate:"YYYY-MM-DD",isoDateTime:"YYYY-MM-DDTHH:mm:ssZ",shortTime:"HH:mm",mediumTime:"HH:mm:ss",longTime:"HH:mm:ss.SSS"});var At=function(t,e,i){if(void 0===e&&(e=Pt.default),void 0===i&&(i={}),"number"==typeof t&&(t=new Date(t)),"[object Date]"!==Object.prototype.toString.call(t)||isNaN(t.getTime()))throw new Error("Invalid Date pass to format");var n=[];e=(e=Pt[e]||e).replace(ft,(function(t,e){return n.push(e),"@@@"}));var r=_t(_t({},St),i);return(e=e.replace(dt,(function(e){return kt[e](t,r)}))).replace(/@@@/g,(function(){return n.shift()}))};function Nt(t){var e=t.split(":").map(Number);return 3600*e[0]+60*e[1]+e[2]}var jt=function(){try{(new Date).toLocaleDateString("i")}catch(t){return"RangeError"===t.name}return!1}()?function(t,e){return t.toLocaleDateString(e,{year:"numeric",month:"long",day:"numeric"})}:function(t){return At(t,"mediumDate")},$t=function(){try{(new Date).toLocaleString("i")}catch(t){return"RangeError"===t.name}return!1}()?function(t,e){return t.toLocaleString(e,{year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"2-digit"})}:function(t){return At(t,"haDateTime")},Ht=function(){try{(new Date).toLocaleTimeString("i")}catch(t){return"RangeError"===t.name}return!1}()?function(t,e){return t.toLocaleTimeString(e,{hour:"numeric",minute:"2-digit"})}:function(t){return At(t,"shortTime")},Rt=function(t){return t<10?"0"+t:t};function Vt(t){return t.substr(0,t.indexOf("."))}var Dt="hass:bookmark",Lt=["closed","locked","off"],Ft=new Set(["fan","input_boolean","light","switch","group","automation"]),It=function(t,e,i,n){n=n||{},i=null==i?{}:i;var r=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=i,t.dispatchEvent(r),r},zt=new Set(["call-service","divider","section","weblink","cast","select"]),Ut={alert:"toggle",automation:"toggle",climate:"climate",cover:"cover",fan:"toggle",group:"group",input_boolean:"toggle",input_number:"input-number",input_select:"input-select",input_text:"input-text",light:"toggle",lock:"lock",media_player:"media-player",remote:"toggle",scene:"scene",script:"script",sensor:"sensor",timer:"timer",switch:"toggle",vacuum:"toggle",water_heater:"climate",input_datetime:"input-datetime"},qt={alert:"hass:alert",automation:"hass:playlist-play",calendar:"hass:calendar",camera:"hass:video",climate:"hass:thermostat",configurator:"hass:settings",conversation:"hass:text-to-speech",device_tracker:"hass:account",fan:"hass:fan",group:"hass:google-circles-communities",history_graph:"hass:chart-line",homeassistant:"hass:home-assistant",homekit:"hass:home-automation",image_processing:"hass:image-filter-frames",input_boolean:"hass:drawing",input_datetime:"hass:calendar-clock",input_number:"hass:ray-vertex",input_select:"hass:format-list-bulleted",input_text:"hass:textbox",light:"hass:lightbulb",mailbox:"hass:mailbox",notify:"hass:comment-alert",person:"hass:account",plant:"hass:flower",proximity:"hass:apple-safari",remote:"hass:remote",scene:"hass:google-pages",script:"hass:file-document",sensor:"hass:eye",simple_alarm:"hass:bell",sun:"hass:white-balance-sunny",switch:"hass:flash",timer:"hass:timer",updater:"hass:cloud-upload",vacuum:"hass:robot-vacuum",water_heater:"hass:thermometer",weblink:"hass:open-in-new"};function Yt(t,e){if(t in qt)return qt[t];switch(t){case"alarm_control_panel":switch(e){case"armed_home":return"hass:bell-plus";case"armed_night":return"hass:bell-sleep";case"disarmed":return"hass:bell-outline";case"triggered":return"hass:bell-ring";default:return"hass:bell"}case"binary_sensor":return e&&"off"===e?"hass:radiobox-blank":"hass:checkbox-marked-circle";case"cover":return"closed"===e?"hass:window-closed":"hass:window-open";case"lock":return e&&"unlocked"===e?"hass:lock-open":"hass:lock";case"media_player":return e&&"off"!==e&&"idle"!==e?"hass:cast-connected":"hass:cast";case"zwave":switch(e){case"dead":return"hass:emoticon-dead";case"sleeping":return"hass:sleep";case"initializing":return"hass:timer-sand";default:return"hass:z-wave"}default:return console.warn("Unable to find icon for domain "+t+" ("+e+")"),Dt}}var Bt=function(t){It(window,"haptic",t)},Wt=function(t,e){return function(t,e,i){void 0===i&&(i=!0);var n,r=Vt(e),s="group"===r?"homeassistant":r;switch(r){case"lock":n=i?"unlock":"lock";break;case"cover":n=i?"open_cover":"close_cover";break;default:n=i?"turn_on":"turn_off"}return t.callService(s,n,{entity_id:e})}(t,e,Lt.includes(t.states[e].state))},Gt=function(t,e,i,n,r){var s;if(r&&i.double_tap_action?s=i.double_tap_action:n&&i.hold_action?s=i.hold_action:!n&&i.tap_action&&(s=i.tap_action),s||(s={action:"more-info"}),!s.confirmation||s.confirmation.exemptions&&s.confirmation.exemptions.some((function(t){return t.user===e.user.id}))||confirm(s.confirmation.text||"Are you sure you want to "+s.action+"?"))switch(s.action){case"more-info":(s.entity||i.entity||i.camera_image)&&(It(t,"hass-more-info",{entityId:s.entity?s.entity:i.entity?i.entity:i.camera_image}),s.haptic&&Bt(s.haptic));break;case"navigate":s.navigation_path&&(function(t,e,i){void 0===i&&(i=!1),i?history.replaceState(null,"",e):history.pushState(null,"",e),It(window,"location-changed",{replace:i})}(0,s.navigation_path),s.haptic&&Bt(s.haptic));break;case"url":s.url_path&&window.open(s.url_path),s.haptic&&Bt(s.haptic);break;case"toggle":i.entity&&(Wt(e,i.entity),s.haptic&&Bt(s.haptic));break;case"call-service":if(!s.service)return;var a=s.service.split(".",2),o=a[0],l=a[1],c=Object.assign({},s.service_data);"entity"===c.entity_id&&(c.entity_id=i.entity),e.callService(o,l,c),s.haptic&&Bt(s.haptic);break;case"fire-dom-event":It(t,"ll-custom",s),s.haptic&&Bt(s.haptic)}},Zt={humidity:"hass:water-percent",illuminance:"hass:brightness-5",temperature:"hass:thermometer",pressure:"hass:gauge",power:"hass:flash",signal_strength:"hass:wifi"},Jt={binary_sensor:function(t){var e=t.state&&"off"===t.state;switch(t.attributes.device_class){case"battery":return e?"hass:battery":"hass:battery-outline";case"cold":return e?"hass:thermometer":"hass:snowflake";case"connectivity":return e?"hass:server-network-off":"hass:server-network";case"door":return e?"hass:door-closed":"hass:door-open";case"garage_door":return e?"hass:garage":"hass:garage-open";case"gas":case"power":case"problem":case"safety":case"smoke":return e?"hass:shield-check":"hass:alert";case"heat":return e?"hass:thermometer":"hass:fire";case"light":return e?"hass:brightness-5":"hass:brightness-7";case"lock":return e?"hass:lock":"hass:lock-open";case"moisture":return e?"hass:water-off":"hass:water";case"motion":return e?"hass:walk":"hass:run";case"occupancy":return e?"hass:home-outline":"hass:home";case"opening":return e?"hass:square":"hass:square-outline";case"plug":return e?"hass:power-plug-off":"hass:power-plug";case"presence":return e?"hass:home-outline":"hass:home";case"sound":return e?"hass:music-note-off":"hass:music-note";case"vibration":return e?"hass:crop-portrait":"hass:vibrate";case"window":return e?"hass:window-closed":"hass:window-open";default:return e?"hass:radiobox-blank":"hass:checkbox-marked-circle"}},cover:function(t){var e="closed"!==t.state;switch(t.attributes.device_class){case"garage":return e?"hass:garage-open":"hass:garage";case"door":return e?"hass:door-open":"hass:door-closed";case"shutter":return e?"hass:window-shutter-open":"hass:window-shutter";case"blind":return e?"hass:blinds-open":"hass:blinds";case"window":return e?"hass:window-open":"hass:window-closed";default:return Yt("cover",t.state)}},sensor:function(t){var e=t.attributes.device_class;if(e&&e in Zt)return Zt[e];if("battery"===e){var i=Number(t.state);if(isNaN(i))return"hass:battery-unknown";var n=10*Math.round(i/10);return n>=100?"hass:battery":n<=0?"hass:battery-alert":"hass:battery-"+n}var r=t.attributes.unit_of_measurement;return"°C"===r||"°F"===r?"hass:thermometer":Yt("sensor")},input_datetime:function(t){return t.attributes.has_date?t.attributes.has_time?Yt("input_datetime"):"hass:calendar":"hass:clock"}};const Xt=(t,e,i,n)=>{n=n||{},i=null==i?{}:i;const r=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=i,t.dispatchEvent(r),r},Kt=(t,e)=>{if(t===e)return!0;if(t&&e&&"object"==typeof t&&"object"==typeof e){if(t.constructor!==e.constructor)return!1;let i,n;if(Array.isArray(t)){if(n=t.length,n!==e.length)return!1;for(i=n;0!=i--;)if(!Kt(t[i],e[i]))return!1;return!0}if(t instanceof Map&&e instanceof Map){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;for(i of t.entries())if(!Kt(i[1],e.get(i[0])))return!1;return!0}if(t instanceof Set&&e instanceof Set){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;return!0}if(ArrayBuffer.isView(t)&&ArrayBuffer.isView(e)){if(n=t.length,n!==e.length)return!1;for(i=n;0!=i--;)if(t[i]!==e[i])return!1;return!0}if(t.constructor===RegExp)return t.source===e.source&&t.flags===e.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===e.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===e.toString();const r=Object.keys(t);if(n=r.length,n!==Object.keys(e).length)return!1;for(i=n;0!=i--;)if(!Object.prototype.hasOwnProperty.call(e,r[i]))return!1;for(i=n;0!=i--;){const n=r[i];if(!Kt(t[n],e[n]))return!1}return!0}return t!=t&&e!=e},Qt="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0;class te extends HTMLElement{constructor(){super(),this.holdTime=500,this.held=!1,this.cancelled=!1,this.isRepeating=!1,this.ripple=document.createElement("mwc-ripple")}connectedCallback(){Object.assign(this.style,{position:"absolute",width:Qt?"100px":"50px",height:Qt?"100px":"50px",transform:"translate(-50%, -50%)",pointerEvents:"none",zIndex:"999"}),this.appendChild(this.ripple),this.ripple.primary=!0,["touchcancel","mouseout","mouseup","touchmove","mousewheel","wheel","scroll"].forEach((t=>{document.addEventListener(t,(()=>{this.cancelled=!0,this.timer&&(this.stopAnimation(),clearTimeout(this.timer),this.timer=void 0,this.isRepeating&&this.repeatTimeout&&(clearInterval(this.repeatTimeout),this.isRepeating=!1))}),{passive:!0})}))}bind(t,e){t.actionHandler&&Kt(e,t.actionHandler.options)||(t.actionHandler?(t.removeEventListener("touchstart",t.actionHandler.start),t.removeEventListener("touchend",t.actionHandler.end),t.removeEventListener("touchcancel",t.actionHandler.end),t.removeEventListener("mousedown",t.actionHandler.start),t.removeEventListener("click",t.actionHandler.end),t.removeEventListener("keyup",t.actionHandler.handleEnter)):t.addEventListener("contextmenu",(t=>{const e=t||window.event;return e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0,e.returnValue=!1,!1})),t.actionHandler={options:e},e.disabled||(t.actionHandler.start=i=>{let n,r;this.cancelled=!1,i.touches?(n=i.touches[0].pageX,r=i.touches[0].pageY):(n=i.pageX,r=i.pageY),e.hasHold&&(this.held=!1,this.timer=window.setTimeout((()=>{this.startAnimation(n,r),this.held=!0,e.repeat&&!this.isRepeating&&(this.isRepeating=!0,this.repeatTimeout=setInterval((()=>{Xt(t,"action",{action:"hold"})}),e.repeat))}),this.holdTime))},t.actionHandler.end=t=>{if(["touchend","touchcancel"].includes(t.type)&&this.cancelled)return void(this.isRepeating&&this.repeatTimeout&&(clearInterval(this.repeatTimeout),this.isRepeating=!1));const i=t.target;t.cancelable&&t.preventDefault(),e.hasHold&&(clearTimeout(this.timer),this.isRepeating&&this.repeatTimeout&&clearInterval(this.repeatTimeout),this.isRepeating=!1,this.stopAnimation(),this.timer=void 0),e.hasHold&&this.held?e.repeat||Xt(i,"action",{action:"hold"}):e.hasDoubleClick?"click"===t.type&&t.detail<2||!this.dblClickTimeout?this.dblClickTimeout=window.setTimeout((()=>{this.dblClickTimeout=void 0,Xt(i,"action",{action:"tap"})}),250):(clearTimeout(this.dblClickTimeout),this.dblClickTimeout=void 0,Xt(i,"action",{action:"double_tap"})):Xt(i,"action",{action:"tap"})},t.actionHandler.handleEnter=t=>{13===t.keyCode&&t.currentTarget.actionHandler.end(t)},t.addEventListener("touchstart",t.actionHandler.start,{passive:!0}),t.addEventListener("touchend",t.actionHandler.end),t.addEventListener("touchcancel",t.actionHandler.end),t.addEventListener("mousedown",t.actionHandler.start,{passive:!0}),t.addEventListener("click",t.actionHandler.end),t.addEventListener("keyup",t.actionHandler.handleEnter)))}startAnimation(t,e){Object.assign(this.style,{left:t+"px",top:e+"px",display:null}),this.ripple.disabled=!1,this.ripple.startPress(),this.ripple.unbounded=!0}stopAnimation(){this.ripple.endPress(),this.ripple.disabled=!0,this.style.display="none"}}customElements.define("button-card-action-handler",te);const ee=(t,e)=>{const i=(()=>{const t=document.body;if(t.querySelector("button-card-action-handler"))return t.querySelector("button-card-action-handler");const e=document.createElement("button-card-action-handler");return t.appendChild(e),e})();i&&i.bind(t,e)},ie=m(((t={})=>e=>{ee(e.committer.element,t)}));function ne(t,e){(function(t){return"string"==typeof t&&t.includes(".")&&1===parseFloat(t)})(t)&&(t="100%");var i=function(t){return"string"==typeof t&&t.includes("%")}(t);return t=360===e?t:Math.min(e,Math.max(0,parseFloat(t))),i&&(t=parseInt(String(t*e),10)/100),Math.abs(t-e)<1e-6?1:t=360===e?(t<0?t%e+e:t%e)/parseFloat(String(e)):t%e/parseFloat(String(e))}function re(t){return Math.min(1,Math.max(0,t))}function se(t){return t=parseFloat(t),(isNaN(t)||t<0||t>1)&&(t=1),t}function ae(t){return t<=1?100*Number(t)+"%":t}function oe(t){return 1===t.length?"0"+t:String(t)}function le(t,e,i){t=ne(t,255),e=ne(e,255),i=ne(i,255);var n=Math.max(t,e,i),r=Math.min(t,e,i),s=0,a=0,o=(n+r)/2;if(n===r)a=0,s=0;else{var l=n-r;switch(a=o>.5?l/(2-n-r):l/(n+r),n){case t:s=(e-i)/l+(e1&&(i-=1),i<1/6?t+6*i*(e-t):i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}function he(t,e,i){t=ne(t,255),e=ne(e,255),i=ne(i,255);var n=Math.max(t,e,i),r=Math.min(t,e,i),s=0,a=n,o=n-r,l=0===n?0:o/n;if(n===r)s=0;else{switch(n){case t:s=(e-i)/o+(e>16,g:(65280&t)>>8,b:255&t}}(e)),this.originalInput=e;var r=me(e);this.originalInput=e,this.r=r.r,this.g=r.g,this.b=r.b,this.a=r.a,this.roundA=Math.round(100*this.a)/100,this.format=null!==(n=i.format)&&void 0!==n?n:r.format,this.gradientType=i.gradientType,this.r<1&&(this.r=Math.round(this.r)),this.g<1&&(this.g=Math.round(this.g)),this.b<1&&(this.b=Math.round(this.b)),this.isValid=r.ok}return t.prototype.isDark=function(){return this.getBrightness()<128},t.prototype.isLight=function(){return!this.isDark()},t.prototype.getBrightness=function(){var t=this.toRgb();return(299*t.r+587*t.g+114*t.b)/1e3},t.prototype.getLuminance=function(){var t=this.toRgb(),e=t.r/255,i=t.g/255,n=t.b/255;return.2126*(e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4))+.7152*(i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4))+.0722*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))},t.prototype.getAlpha=function(){return this.a},t.prototype.setAlpha=function(t){return this.a=se(t),this.roundA=Math.round(100*this.a)/100,this},t.prototype.toHsv=function(){var t=he(this.r,this.g,this.b);return{h:360*t.h,s:t.s,v:t.v,a:this.a}},t.prototype.toHsvString=function(){var t=he(this.r,this.g,this.b),e=Math.round(360*t.h),i=Math.round(100*t.s),n=Math.round(100*t.v);return 1===this.a?"hsv("+e+", "+i+"%, "+n+"%)":"hsva("+e+", "+i+"%, "+n+"%, "+this.roundA+")"},t.prototype.toHsl=function(){var t=le(this.r,this.g,this.b);return{h:360*t.h,s:t.s,l:t.l,a:this.a}},t.prototype.toHslString=function(){var t=le(this.r,this.g,this.b),e=Math.round(360*t.h),i=Math.round(100*t.s),n=Math.round(100*t.l);return 1===this.a?"hsl("+e+", "+i+"%, "+n+"%)":"hsla("+e+", "+i+"%, "+n+"%, "+this.roundA+")"},t.prototype.toHex=function(t){return void 0===t&&(t=!1),ue(this.r,this.g,this.b,t)},t.prototype.toHexString=function(t){return void 0===t&&(t=!1),"#"+this.toHex(t)},t.prototype.toHex8=function(t){return void 0===t&&(t=!1),function(t,e,i,n,r){var s,a=[oe(Math.round(t).toString(16)),oe(Math.round(e).toString(16)),oe(Math.round(i).toString(16)),oe((s=n,Math.round(255*parseFloat(s)).toString(16)))];return r&&a[0].startsWith(a[0].charAt(1))&&a[1].startsWith(a[1].charAt(1))&&a[2].startsWith(a[2].charAt(1))&&a[3].startsWith(a[3].charAt(1))?a[0].charAt(0)+a[1].charAt(0)+a[2].charAt(0)+a[3].charAt(0):a.join("")}(this.r,this.g,this.b,this.a,t)},t.prototype.toHex8String=function(t){return void 0===t&&(t=!1),"#"+this.toHex8(t)},t.prototype.toRgb=function(){return{r:Math.round(this.r),g:Math.round(this.g),b:Math.round(this.b),a:this.a}},t.prototype.toRgbString=function(){var t=Math.round(this.r),e=Math.round(this.g),i=Math.round(this.b);return 1===this.a?"rgb("+t+", "+e+", "+i+")":"rgba("+t+", "+e+", "+i+", "+this.roundA+")"},t.prototype.toPercentageRgb=function(){var t=function(t){return Math.round(100*ne(t,255))+"%"};return{r:t(this.r),g:t(this.g),b:t(this.b),a:this.a}},t.prototype.toPercentageRgbString=function(){var t=function(t){return Math.round(100*ne(t,255))};return 1===this.a?"rgb("+t(this.r)+"%, "+t(this.g)+"%, "+t(this.b)+"%)":"rgba("+t(this.r)+"%, "+t(this.g)+"%, "+t(this.b)+"%, "+this.roundA+")"},t.prototype.toName=function(){if(0===this.a)return"transparent";if(this.a<1)return!1;for(var t="#"+ue(this.r,this.g,this.b,!1),e=0,i=Object.entries(fe);e=0;return e||!n||!t.startsWith("hex")&&"name"!==t?("rgb"===t&&(i=this.toRgbString()),"prgb"===t&&(i=this.toPercentageRgbString()),"hex"!==t&&"hex6"!==t||(i=this.toHexString()),"hex3"===t&&(i=this.toHexString(!0)),"hex4"===t&&(i=this.toHex8String(!0)),"hex8"===t&&(i=this.toHex8String()),"name"===t&&(i=this.toName()),"hsl"===t&&(i=this.toHslString()),"hsv"===t&&(i=this.toHsvString()),i||this.toHexString()):"name"===t&&0===this.a?this.toName():this.toRgbString()},t.prototype.toNumber=function(){return(Math.round(this.r)<<16)+(Math.round(this.g)<<8)+Math.round(this.b)},t.prototype.clone=function(){return new t(this.toString())},t.prototype.lighten=function(e){void 0===e&&(e=10);var i=this.toHsl();return i.l+=e/100,i.l=re(i.l),new t(i)},t.prototype.brighten=function(e){void 0===e&&(e=10);var i=this.toRgb();return i.r=Math.max(0,Math.min(255,i.r-Math.round(-e/100*255))),i.g=Math.max(0,Math.min(255,i.g-Math.round(-e/100*255))),i.b=Math.max(0,Math.min(255,i.b-Math.round(-e/100*255))),new t(i)},t.prototype.darken=function(e){void 0===e&&(e=10);var i=this.toHsl();return i.l-=e/100,i.l=re(i.l),new t(i)},t.prototype.tint=function(t){return void 0===t&&(t=10),this.mix("white",t)},t.prototype.shade=function(t){return void 0===t&&(t=10),this.mix("black",t)},t.prototype.desaturate=function(e){void 0===e&&(e=10);var i=this.toHsl();return i.s-=e/100,i.s=re(i.s),new t(i)},t.prototype.saturate=function(e){void 0===e&&(e=10);var i=this.toHsl();return i.s+=e/100,i.s=re(i.s),new t(i)},t.prototype.greyscale=function(){return this.desaturate(100)},t.prototype.spin=function(e){var i=this.toHsl(),n=(i.h+e)%360;return i.h=n<0?360+n:n,new t(i)},t.prototype.mix=function(e,i){void 0===i&&(i=50);var n=this.toRgb(),r=new t(e).toRgb(),s=i/100;return new t({r:(r.r-n.r)*s+n.r,g:(r.g-n.g)*s+n.g,b:(r.b-n.b)*s+n.b,a:(r.a-n.a)*s+n.a})},t.prototype.analogous=function(e,i){void 0===e&&(e=6),void 0===i&&(i=30);var n=this.toHsl(),r=360/i,s=[this];for(n.h=(n.h-(r*e>>1)+720)%360;--e;)n.h=(n.h+r)%360,s.push(new t(n));return s},t.prototype.complement=function(){var e=this.toHsl();return e.h=(e.h+180)%360,new t(e)},t.prototype.monochromatic=function(e){void 0===e&&(e=6);for(var i=this.toHsv(),n=i.h,r=i.s,s=i.v,a=[],o=1/e;e--;)a.push(new t({h:n,s:r,v:s})),s=(s+o)%1;return a},t.prototype.splitcomplement=function(){var e=this.toHsl(),i=e.h;return[this,new t({h:(i+72)%360,s:e.s,l:e.l}),new t({h:(i+216)%360,s:e.s,l:e.l})]},t.prototype.triad=function(){return this.polyad(3)},t.prototype.tetrad=function(){return this.polyad(4)},t.prototype.polyad=function(e){for(var i=this.toHsl(),n=i.h,r=[this],s=360/e,a=1;at&&"object"==typeof t;return t.reduce(((t,i)=>(Object.keys(i).forEach((n=>{const r=t[n],s=i[n];Array.isArray(r)&&Array.isArray(s)?t[n]=r.concat(...s):e(r)&&e(s)?t[n]=Te(r,s):t[n]=s})),t)),{})}function Me(t,e){let i=[];return t&&t.forEach((t=>{let n=t;e&&e.forEach((e=>{e.id&&t.id&&e.id==t.id&&(n=Te(n,e))})),i.push(n)})),e&&(i=i.concat(e.filter((e=>!t||!t.find((t=>!(!t.id||!e.id)&&t.id==e.id)))))),i}const Ce=((t,...e)=>{const i=e.reduce(((e,i,n)=>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[n+1]),t[0]);return new et(i,tt)})` + :host { + position: relative; + display: block; + } + ha-card { + cursor: pointer; + overflow: hidden; + box-sizing: border-box; + position: relative; + display: flex; + justify-content: center; + align-items: center; + line-height: normal; + + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome, Opera and Firefox */ + } + ha-card.disabled { + pointer-events: none; + cursor: default; + } + :host(.tooltip) .tooltiptext { + pointer-events: none; + opacity: 0; + text-align: center; + padding: 4px; + border-radius: var(--ha-card-border-radius, 4px); + box-shadow: var( + --ha-card-box-shadow, + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12) + ); + background: var(--ha-card-background, var(--card-background-color, white)); + border: 1px solid var(--primary-text-color); + color: var(--primary-text-color); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 9999; + } + :host(.tooltip:hover) span.tooltiptext { + opacity: 1; + transition-delay: 1.5s; + } + ha-icon { + display: inline-block; + margin: auto; + --mdc-icon-size: 100%; + --iron-icon-width: 100%; + --iron-icon-height: 100%; + } + ha-card.button-card-main { + padding: 4% 0px; + text-transform: none; + font-weight: 400; + font-size: 1.2rem; + align-items: center; + text-align: center; + letter-spacing: normal; + width: 100%; + } + .ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + #overlay { + align-items: flex-start; + justify-content: flex-end; + padding: 8px 7px; + opacity: 0.5; + /* DO NOT override items below */ + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 50; + display: flex; + } + #lock { + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + margin: unset; + width: 24px; + } + .invalid { + animation: blink 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite; + } + .hidden { + visibility: hidden; + opacity: 0; + transition: visibility 0s 1s, opacity 1s linear; + } + @keyframes blink { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + @-webkit-keyframes rotating /* Safari and Chrome */ { + from { + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } + } + @keyframes rotating { + from { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } + } + [rotating] { + -webkit-animation: rotating 2s linear infinite; + -moz-animation: rotating 2s linear infinite; + -ms-animation: rotating 2s linear infinite; + -o-animation: rotating 2s linear infinite; + animation: rotating 2s linear infinite; + } + + #container { + display: grid; + width: 100%; + height: 100%; + text-align: center; + align-items: center; + } + #img-cell { + display: flex; + grid-area: i; + height: 100%; + width: 100%; + max-width: 100%; + max-height: 100%; + align-self: center; + justify-self: center; + overflow: hidden; + justify-content: center; + align-items: center; + position: relative; + } + + ha-icon#icon { + height: 100%; + width: 100%; + max-height: 100%; + position: absolute; + } + img#icon { + display: block; + height: auto; + width: 100%; + position: absolute; + } + #name { + grid-area: n; + max-width: 100%; + align-self: center; + justify-self: center; + /* margin: auto; */ + } + #state { + grid-area: s; + max-width: 100%; + align-self: center; + justify-self: center; + /* margin: auto; */ + } + + #label { + grid-area: l; + max-width: 100%; + align-self: center; + justify-self: center; + } + + #container.vertical { + grid-template-areas: 'i' 'n' 's' 'l'; + grid-template-columns: 1fr; + grid-template-rows: 1fr min-content min-content min-content; + } + /* Vertical No Icon */ + #container.vertical.no-icon { + grid-template-areas: 'n' 's' 'l'; + grid-template-columns: 1fr; + grid-template-rows: 1fr min-content 1fr; + } + #container.vertical.no-icon #state { + align-self: center; + } + #container.vertical.no-icon #name { + align-self: end; + } + #container.vertical.no-icon #label { + align-self: start; + } + + /* Vertical No Icon No Name */ + #container.vertical.no-icon.no-name { + grid-template-areas: 's' 'l'; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } + #container.vertical.no-icon.no-name #state { + align-self: end; + } + #container.vertical.no-icon.no-name #label { + align-self: start; + } + + /* Vertical No Icon No State */ + #container.vertical.no-icon.no-state { + grid-template-areas: 'n' 'l'; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } + #container.vertical.no-icon.no-state #name { + align-self: end; + } + #container.vertical.no-icon.no-state #label { + align-self: start; + } + + /* Vertical No Icon No Label */ + #container.vertical.no-icon.no-label { + grid-template-areas: 'n' 's'; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } + #container.vertical.no-icon.no-label #name { + align-self: end; + } + #container.vertical.no-icon.no-label #state { + align-self: start; + } + + /* Vertical No Icon No Label No Name */ + #container.vertical.no-icon.no-label.no-name { + grid-template-areas: 's'; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + } + #container.vertical.no-icon.no-label.no-name #state { + align-self: center; + } + /* Vertical No Icon No Label No State */ + #container.vertical.no-icon.no-label.no-state { + grid-template-areas: 'n'; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + } + #container.vertical.no-icon.no-label.no-state #name { + align-self: center; + } + + /* Vertical No Icon No Name No State */ + #container.vertical.no-icon.no-name.no-state { + grid-template-areas: 'l'; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + } + #container.vertical.no-icon.no-name.no-state #label { + align-self: center; + } + + #container.icon_name_state { + grid-template-areas: 'i n' 'l l'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr min-content; + } + + #container.icon_name { + grid-template-areas: 'i n' 's s' 'l l'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr min-content min-content; + } + + #container.icon_state { + grid-template-areas: 'i s' 'n n' 'l l'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr min-content min-content; + } + + #container.name_state { + grid-template-areas: 'i' 'n' 'l'; + grid-template-columns: 1fr; + grid-template-rows: 1fr min-content min-content; + } + #container.name_state.no-icon { + grid-template-areas: 'n' 'l'; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } + #container.name_state.no-icon #name { + align-self: end; + } + #container.name_state.no-icon #label { + align-self: start; + } + + #container.name_state.no-icon.no-label { + grid-template-areas: 'n'; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + } + #container.name_state.no-icon.no-label #name { + align-self: center; + } + + /* icon_name_state2nd default */ + #container.icon_name_state2nd { + grid-template-areas: 'i n' 'i s' 'i l'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr min-content 1fr; + } + #container.icon_name_state2nd #name { + align-self: end; + } + #container.icon_name_state2nd #state { + align-self: center; + } + #container.icon_name_state2nd #label { + align-self: start; + } + + /* icon_name_state2nd No Label */ + #container.icon_name_state2nd.no-label { + grid-template-areas: 'i n' 'i s'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr 1fr; + } + #container.icon_name_state2nd #name { + align-self: end; + } + #container.icon_name_state2nd #state { + align-self: start; + } + + /* icon_state_name2nd Default */ + #container.icon_state_name2nd { + grid-template-areas: 'i s' 'i n' 'i l'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr min-content 1fr; + } + #container.icon_state_name2nd #state { + align-self: end; + } + #container.icon_state_name2nd #name { + align-self: center; + } + #container.icon_state_name2nd #label { + align-self: start; + } + + /* icon_state_name2nd No Label */ + #container.icon_state_name2nd.no-label { + grid-template-areas: 'i s' 'i n'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr 1fr; + } + #container.icon_state_name2nd #state { + align-self: end; + } + #container.icon_state_name2nd #name { + align-self: start; + } + + #container.icon_label { + grid-template-areas: 'i l' 'n n' 's s'; + grid-template-columns: 40% 1fr; + grid-template-rows: 1fr min-content min-content; + } + + [style*='--aspect-ratio'] > :first-child { + width: 100%; + } + [style*='--aspect-ratio'] > img { + height: auto; + } + @supports (--custom: property) { + [style*='--aspect-ratio'] { + position: relative; + } + [style*='--aspect-ratio']::before { + content: ''; + display: block; + padding-bottom: calc(100% / (var(--aspect-ratio))); + } + [style*='--aspect-ratio'] > :first-child { + position: absolute; + top: 0; + left: 0; + height: 100%; + } + } +`;const Ee=(t,e,i,n)=>{if(!((t,e,i)=>{const[n,r]=t.split(".",2);return Number(n)>e||Number(n)===e&&Number(r)>=i})(t.config.version,0,109))return function(t,e){let i;const n=xe(e.entity_id);return"binary_sensor"===n?(e.attributes.device_class&&(i=t(`state.${n}.${e.attributes.device_class}.${e.state}`)),i||(i=t(`state.${n}.default.${e.state}`))):i=e.attributes.unit_of_measurement&&!["unknown","unavailable"].includes(e.state)?e.state:"zwave"===n?["initializing","dead"].includes(e.state)?t("state.zwave.query_stage."+e.state,"query_stage",e.attributes.query_stage):t("state.zwave.default."+e.state):t(`state.${n}.${e.state}`),i||(i=t("state.default."+e.state)||t(`component.${n}.state.${e.state}`)||e.state),i}(e,i);if("unknown"===i.state||"unavailable"===i.state)return e("state.default."+i.state);if(i.attributes.unit_of_measurement)return`${i.state} ${i.attributes.unit_of_measurement}`;const r=xe(i.entity_id);if("input_datetime"===r){let t;if(!i.attributes.has_time)return t=new Date(i.attributes.year,i.attributes.month-1,i.attributes.day),jt(t,n);if(!i.attributes.has_date){const e=new Date;return t=new Date(e.getFullYear(),e.getMonth(),e.getDay(),i.attributes.hour,i.attributes.minute),Ht(t,n)}return t=new Date(i.attributes.year,i.attributes.month-1,i.attributes.day,i.attributes.hour,i.attributes.minute),$t(t,n)}return i.attributes.device_class&&e(`component.${r}.state.${i.attributes.device_class}.${i.state}`)||e(`component.${r}.state._.${i.state}`)||i.state};var Pe=Function.prototype.toString,Ae=Object.create,Ne=Object.defineProperty,je=Object.getOwnPropertyDescriptor,$e=Object.getOwnPropertyNames,He=Object.getOwnPropertySymbols,Re=Object.getPrototypeOf,Ve=Object.prototype,De=Ve.hasOwnProperty,Le=Ve.propertyIsEnumerable,Fe="function"==typeof He,Ie="function"==typeof WeakMap,ze=function(t,e){if(!t.constructor)return Ae(null);var i=t.constructor,n=t.__proto__||Re(t);if(i===e.Object)return n===e.Object.prototype?{}:Ae(n);if(~Pe.call(i).indexOf("[native code]"))try{return new i}catch(r){}return Ae(n)},Ue=function(t,e,i,n){var r=ze(t,e);for(var s in n.set(t,r),t)De.call(t,s)&&(r[s]=i(t[s],n));if(Fe){var a=He(t),o=a.length;if(o)for(var l=0,c=void 0;l{Ge&&t(),window.loadCardHelpers&&(Ge=await window.loadCardHelpers(),window.cardHelpers=Ge,t())}));console.info("%c BUTTON-CARD \n%c Version 3.4.2 ","color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray"),window.customCards=window.customCards||[],window.customCards.push({type:"button-card",name:"Button-Card",preview:!1,description:"A massively customizable custom button card"});let Je=class extends nt{constructor(){super(...arguments),this._cards={},this._cardsConfig={},this._entities=[],this._initial_setup_complete=!1,this._rippleHandlers=new rt((()=>this._ripple))}set hass(t){this._hass=t,Object.keys(this._cards).forEach((t=>{this._cards[t].hass=this._hass})),this._initial_setup_complete||this._initConnected()}disconnectedCallback(){super.disconnectedCallback(),this._clearInterval()}connectedCallback(){super.connectedCallback(),this._initial_setup_complete?this._startTimerCountdown():this._initConnected()}_initConnected(){void 0!==this._hass&&void 0!==this._config&&this.isConnected&&(this._initial_setup_complete=!0,this._startTimerCountdown())}_startTimerCountdown(){if(this._config&&this._config.entity&&"timer"===xe(this._config.entity)){const t=this._hass.states[this._config.entity];this._startInterval(t)}}_createCard(t){if(Ge)return Ge.createCardElement(t);{const e=function(t,e){void 0===e&&(e=!1);var i=function(t,e){return n("hui-error-card",{type:"error",error:t,config:e})},n=function(t,e){var n=window.document.createElement(t);try{n.setConfig(e)}catch(n){return console.error(t,n),i(n.message,e)}return n};if(!t||"object"!=typeof t||!e&&!t.type)return i("No type defined",t);var r=t.type;if(r&&r.startsWith("custom:"))r=r.substr("custom:".length);else if(e)if(zt.has(r))r="hui-"+r+"-row";else{if(!t.entity)return i("Invalid config given.",t);var s=t.entity.split(".",1)[0];r="hui-"+(Ut[s]||"text")+"-entity-row"}else r="hui-"+r+"-card";if(customElements.get(r))return n(r,t);var a=i("Custom element doesn't exist: "+t.type+".",t);a.style.display="None";var o=setTimeout((function(){a.style.display=""}),2e3);return customElements.whenDefined(t.type).then((function(){clearTimeout(o),It(a,"ll-rebuild",{},a)})),a}(t);return Ze.then((()=>{It(e,"ll-rebuild",{})})),e}}static get styles(){return Ce}render(){if(!this._config||!this._hass)return D``;this._stateObj=this._config.entity?this._hass.states[this._config.entity]:void 0;try{return this._evaledVariables=this._config.variables?this._objectEvalTemplate(this._stateObj,this._config.variables):void 0,this._cardHtml()}catch(t){t.stack?console.error(t.stack):console.error(t);const e=document.createElement("hui-error-card");return e.setConfig({type:"error",error:t.toString(),origConfig:this._config}),D` ${e} `}}shouldUpdate(t){return!(!this._hasTemplate&&!t.has("_timeRemaining")&&!function(t,e){if(e.has("_config"))return!0;const i=e.get("_hass");if(i)return t._entities.some((function(e){return(null==i?void 0:i.states[e])!==t._hass.states[e]}));return!1}(this,t))&&(this._expandTriggerGroups(),!0)}updated(t){if(super.updated(t),this._config&&this._config.entity&&"timer"===xe(this._config.entity)&&t.has("_hass")){const e=this._hass.states[this._config.entity],i=t.get("_hass");(i?i.states[this._config.entity]:void 0)!==e?this._startInterval(e):e||this._clearInterval()}}_clearInterval(){this._interval&&(window.clearInterval(this._interval),this._interval=void 0)}_startInterval(t){this._clearInterval(),this._calculateRemaining(t),"active"===t.state&&(this._interval=window.setInterval((()=>this._calculateRemaining(t)),1e3))}_calculateRemaining(t){t.attributes.remaining&&(this._timeRemaining=function(t){var e=Nt(t.attributes.remaining);if("active"===t.state){var i=(new Date).getTime(),n=new Date(t.last_changed).getTime();e=Math.max(e-(i-n)/1e3,0)}return e}(t))}_computeTimeDisplay(t){if(t)return function(t){var e=Math.floor(t/3600),i=Math.floor(t%3600/60),n=Math.floor(t%3600%60);return e>0?e+":"+Rt(i)+":"+Rt(n):i>0?i+":"+Rt(n):n>0?""+n:null}(this._timeRemaining||Nt(t.attributes.duration))}_getMatchingConfigState(t){if(!this._config.state)return;const e=this._config.state.find((t=>"template"===t.operator));if(!t&&!e)return;let i;const n=this._config.state.find((e=>{if(!e.operator)return t&&this._getTemplateOrValue(t,e.value)==t.state;switch(e.operator){case"==":return t&&t.state==this._getTemplateOrValue(t,e.value);case"<=":return t&&t.state<=this._getTemplateOrValue(t,e.value);case"<":return t&&t.state=":return t&&t.state>=this._getTemplateOrValue(t,e.value);case">":return t&&t.state>this._getTemplateOrValue(t,e.value);case"!=":return t&&t.state!=this._getTemplateOrValue(t,e.value);case"regex":return!(!t||!t.state.match(this._getTemplateOrValue(t,e.value)));case"template":return this._getTemplateOrValue(t,e.value);case"default":return i=e,!1;default:return!1}}));return!n&&i?i:n}_evalTemplate(t,e){try{return new Function("states","entity","user","hass","variables","html","'use strict'; "+e).call(this,this._hass.states,t,this._hass.user,this._hass,this._evaledVariables,D)}catch(i){const t=e.length<=100?e.trim():e.trim().substring(0,98)+"...";throw i.message=`${i.name}: ${i.message} in '${t}'`,i.name="ButtonCardJSTemplateError",i}}_objectEvalTemplate(t,e){const i=We(e);return this._getTemplateOrValue(t,i)}_getTemplateOrValue(t,e){if(["number","boolean"].includes(typeof e))return e;if(!e)return e;if("object"==typeof e)return Object.keys(e).forEach((i=>{e[i]=this._getTemplateOrValue(t,e[i])})),e;const i=e.trim();return"[[["===i.substring(0,3)&&"]]]"===i.slice(-3)?this._evalTemplate(t,i.slice(3,-3)):e}_getDefaultColorForState(t){switch(t.state){case"on":return this._config.color_on;case"off":return this._config.color_off;default:return this._config.default_color}}_getColorForLightEntity(t,e){let i=this._config.default_color;return t&&("on"===t.state?t.attributes.rgb_color?(i=`rgb(${t.attributes.rgb_color.join(",")})`,t.attributes.brightness&&(i=Oe(i,(t.attributes.brightness+245)/5))):e&&t.attributes.color_temp&&t.attributes.min_mireds&&t.attributes.max_mireds?(i=function(t,e,i){const n=new we("rgb(255, 160, 0)"),r=new we("rgb(166, 209, 255)"),s=new we("white"),a=(t-e)/(i-e)*100;return a<50?Se(r).mix(s,2*a).toRgbString():Se(s).mix(n,2*(a-50)).toRgbString()}(t.attributes.color_temp,t.attributes.min_mireds,t.attributes.max_mireds),t.attributes.brightness&&(i=Oe(i,(t.attributes.brightness+245)/5))):i=t.attributes.brightness?Oe(this._getDefaultColorForState(t),(t.attributes.brightness+245)/5):this._getDefaultColorForState(t):i=this._getDefaultColorForState(t)),i}_buildCssColorAttribute(t,e){let i,n="";return(null==e?void 0:e.color)?n=e.color:"auto"!==this._config.color&&"off"===(null==t?void 0:t.state)?n=this._config.color_off:this._config.color&&(n=this._config.color),i="auto"==n||"auto-no-temperature"==n?this._getColorForLightEntity(t,"auto-no-temperature"!==n):n||(t?this._getDefaultColorForState(t):this._config.default_color),i}_buildIcon(t,e){if(!this._config.show_icon)return;let i;if(null==e?void 0:e.icon)i=e.icon;else if(this._config.icon)i=this._config.icon;else{if(!t)return;i=function(t){if(!t)return Dt;if(t.attributes.icon)return t.attributes.icon;var e=Vt(t.entity_id);return e in Jt?Jt[e](t):Yt(e,t.state)}(t)}return this._getTemplateOrValue(t,i)}_buildEntityPicture(t,e){if(!this._config.show_entity_picture||!t&&!e&&!this._config.entity_picture)return;let i;return(null==e?void 0:e.entity_picture)?i=e.entity_picture:this._config.entity_picture?i=this._config.entity_picture:t&&(i=t.attributes&&t.attributes.entity_picture?t.attributes.entity_picture:void 0),this._getTemplateOrValue(t,i)}_buildStyleGeneric(t,e,i){var n,r;let s={};if((null===(n=this._config.styles)||void 0===n?void 0:n[i])&&(s=Object.assign(s,...this._config.styles[i])),null===(r=null==e?void 0:e.styles)||void 0===r?void 0:r[i]){let t={};t=Object.assign(t,...e.styles[i]),s=Object.assign(Object.assign({},s),t)}return Object.keys(s).forEach((e=>{s[e]=this._getTemplateOrValue(t,s[e])})),s}_buildCustomStyleGeneric(t,e,i){var n,r,s,a;let o={};if((null===(r=null===(n=this._config.styles)||void 0===n?void 0:n.custom_fields)||void 0===r?void 0:r[i])&&(o=Object.assign(o,...this._config.styles.custom_fields[i])),null===(a=null===(s=null==e?void 0:e.styles)||void 0===s?void 0:s.custom_fields)||void 0===a?void 0:a[i]){let t={};t=Object.assign(t,...e.styles.custom_fields[i]),o=Object.assign(Object.assign({},o),t)}return Object.keys(o).forEach((e=>{o[e]=this._getTemplateOrValue(t,o[e])})),o}_buildName(t,e){if(!1===this._config.show_name)return;let i;var n;return(null==e?void 0:e.name)?i=e.name:this._config.name?i=this._config.name:t&&(i=t.attributes&&t.attributes.friendly_name?t.attributes.friendly_name:(n=t.entity_id).substr(n.indexOf(".")+1)),this._getTemplateOrValue(t,i)}_buildStateString(t){var e;let i;if(this._config.show_state&&t&&t.state){const n=this._buildUnits(t);n?i=`${t.state} ${n}`:"timer"===xe(t.entity_id)?"idle"===t.state||0===this._timeRemaining?i=Ee(this._hass,this._hass.localize,t,this._hass.language):(i=this._computeTimeDisplay(t),"paused"===t.state&&(i+=` (${Ee(this._hass,this._hass.localize,t,this._hass.language)})`)):i=(null===(e=this._config)||void 0===e?void 0:e.show_units)||"sensor"!==xe(t.entity_id)?Ee(this._hass,this._hass.localize,t,this._hass.language):t.state}return i}_buildUnits(t){var e;let i;return t&&this._config.show_units&&(i=(null===(e=t.attributes)||void 0===e?void 0:e.unit_of_measurement)&&!this._config.units?t.attributes.unit_of_measurement:this._config.units?this._config.units:void 0),i}_buildLastChanged(t,e){return this._config.show_last_changed&&t?D` + + `:void 0}_buildLabel(t,e){if(!this._config.show_label)return;let i;return i=(null==e?void 0:e.label)?e.label:this._config.label,this._getTemplateOrValue(t,i)}_buildCustomFields(t,e){let i=D``;const n={},r={};return this._config.custom_fields&&Object.keys(this._config.custom_fields).forEach((e=>{const i=this._config.custom_fields[e];i.card?r[e]=this._objectEvalTemplate(t,i.card):n[e]=this._getTemplateOrValue(t,i)})),(null==e?void 0:e.custom_fields)&&Object.keys(e.custom_fields).forEach((i=>{const s=e.custom_fields[i];s.card?r[i]=this._objectEvalTemplate(t,s.card):n[i]=this._getTemplateOrValue(t,s)})),Object.keys(n).forEach((r=>{if(null!=n[r]){const s=Object.assign(Object.assign({},this._buildCustomStyleGeneric(t,e,r)),{"grid-area":r});i=D` + ${i} +
+ ${n[r]&&"html"===n[r].type?n[r]:lt(n[r])} +
+ `}})),Object.keys(r).forEach((n=>{if(null!=r[n]){const s=Object.assign(Object.assign({},this._buildCustomStyleGeneric(t,e,n)),{"grid-area":n});let a;Kt(this._cardsConfig[n],r[n])?a=this._cards[n]:(a=this._createCard(r[n]),this._cards[n]=a,this._cardsConfig[n]=We(r[n])),a.hass=this._hass,i=D` + ${i} +
+ ${a} +
+ `}})),i}_isClickable(t){let e=!0;const i=this._getTemplateOrValue(t,this._config.tap_action.action),n=this._getTemplateOrValue(t,this._config.hold_action.action),r=this._getTemplateOrValue(t,this._config.double_tap_action.action);return e="none"!=i||"none"!=n||"none"!=r,e}_rotate(t){return!!(null==t?void 0:t.spin)}_blankCardColoredHtml(t){const e=Object.assign({background:"none","box-shadow":"none"},t);return D` + +
+
+ `}_cardHtml(){var t,e;const i=this._getMatchingConfigState(this._stateObj),n=this._buildCssColorAttribute(this._stateObj,i);let r=n,s={},a={};const o={},l=this._buildStyleGeneric(this._stateObj,i,"lock"),c=this._buildStyleGeneric(this._stateObj,i,"card"),h=this._buildStyleGeneric(this._stateObj,i,"tooltip"),u={"button-card-main":!0,disabled:!this._isClickable(this._stateObj)};switch((null===(t=this._config)||void 0===t?void 0:t.tooltip)&&this.classList.add("tooltip"),c.width&&(this.style.setProperty("flex","0 0 auto"),this.style.setProperty("max-width","fit-content")),this._config.color_type){case"blank-card":return this._blankCardColoredHtml(c);case"card":case"label-card":{const t=function(t){const e=new we(ke(t));return e.isValid&&e.getLuminance()>.5?"rgb(62, 62, 62)":"rgb(234, 234, 234)"}(n);s.color=t,a.color=t,s["background-color"]=n,s=Object.assign(Object.assign({},s),c),r="inherit";break}default:s=c}this._config.aspect_ratio?(o["--aspect-ratio"]=this._config.aspect_ratio,s.position="absolute"):o.display="inline",this.style.setProperty("--button-card-light-color",this._getColorForLightEntity(this._stateObj,!0)),this.style.setProperty("--button-card-light-color-no-temperature",this._getColorForLightEntity(this._stateObj,!1)),a=Object.assign(Object.assign({},a),l);const d=this._config.extra_styles?D` + + `:D``;return D` + ${d} +
+ + ${this._buttonContent(this._stateObj,i,r)} + + +
+ ${this._getLock(a)} + ${(null===(e=this._config)||void 0===e?void 0:e.tooltip)?D` + + ${this._getTemplateOrValue(this._stateObj,this._config.tooltip)} + + `:""} + `}_getLock(t){return this._config.lock&&this._getTemplateOrValue(this._stateObj,this._config.lock.enabled)?D` +
+ +
+ `:D``}_buttonContent(t,e,i){const n=this._buildName(t,e),r=this._config.show_state&&this._config.state_display?this._getTemplateOrValue(t,this._config.state_display):void 0,s=r||this._buildStateString(t),a=function(t,e){if(!t&&!e)return;let i;return i=e?t?`${t}: ${e}`:e:t,i}(n,s);switch(this._config.layout){case"icon_name_state":case"name_state":return this._gridHtml(t,e,this._config.layout,i,a,void 0);default:return this._gridHtml(t,e,this._config.layout,i,n,s)}}_gridHtml(t,e,i,n,r,s){const a=this._getIconHtml(t,e,n),o=[i],l=this._buildLabel(t,e),c=this._buildStyleGeneric(t,e,"name"),h=this._buildStyleGeneric(t,e,"state"),u=this._buildStyleGeneric(t,e,"label"),d=this._buildLastChanged(t,u),p=this._buildStyleGeneric(t,e,"grid");return a||o.push("no-icon"),r||o.push("no-name"),s||o.push("no-state"),l||d||o.push("no-label"),D` +
+ ${a||""} + ${r?D` +
+ ${"html"===r.type?r:lt(r)} +
+ `:""} + ${s?D` +
+ ${"html"===s.type?s:lt(s)} +
+ `:""} + ${l&&!d?D` +
+ ${"html"===l.type?l:lt(l)} +
+ `:""} + ${d||""} ${this._buildCustomFields(t,e)} +
+ `}_getIconHtml(t,e,i){const n=this._buildIcon(t,e),r=this._buildEntityPicture(t,e),s=this._buildStyleGeneric(t,e,"entity_picture"),a=this._buildStyleGeneric(t,e,"icon"),o=this._buildStyleGeneric(t,e,"img_cell"),l=this._buildStyleGeneric(t,e,"card"),c=Object.assign({color:i,width:this._config.size,position:this._config.aspect_ratio||l.height?"absolute":"relative"},a),h=Object.assign(Object.assign({},c),s),u=this._buildLiveStream(h);return n||r?D` +
+ ${!n||r||u?"":D` + + `} + ${u||""} + ${r&&!u?D` + + `:""} +
+ `:void 0}_buildLiveStream(t){return this._config.show_live_stream&&this._config.entity&&"camera"===xe(this._config.entity)?D` + + `:void 0}_configFromLLTemplates(t,e){const i=e.template;if(!i)return e;let n,r={};const s=i&&Array.isArray(i)?i:[i];return null==s||s.forEach((e=>{var i;if(!(null===(i=t.config.button_card_templates)||void 0===i?void 0:i[e]))throw new Error(`Button-card template '${e}' is missing!`);const s=this._configFromLLTemplates(t,t.config.button_card_templates[e]);r=Te(r,s),n=Me(n,s.state)})),r=Te(r,e),r.state=Me(n,e.state),r}setConfig(t){if(!t)throw new Error("Invalid configuration");this._cards={},this._cardsConfig={};const e=function(){var t=document.querySelector("home-assistant");if(t=(t=(t=(t=(t=(t=(t=(t=t&&t.shadowRoot)&&t.querySelector("home-assistant-main"))&&t.shadowRoot)&&t.querySelector("app-drawer-layout partial-panel-resolver"))&&t.shadowRoot||t)&&t.querySelector("ha-panel-lovelace"))&&t.shadowRoot)&&t.querySelector("hui-root")){var e=t.lovelace;return e.current_view=t.___curView,e}return null}()||function(){let t=document.querySelector("hc-main");if(t=t&&t.shadowRoot,t=t&&t.querySelector("hc-lovelace"),t=t&&t.shadowRoot,t=t&&(t.querySelector("hui-view")||t.querySelector("hui-panel-view")),t){const e=t.lovelace;return e.current_view=t.___curView,e}return null}();let i=We(t);i=this._configFromLLTemplates(e,i),this._config=Object.assign(Object.assign({type:"custom:button-card",group_expand:!1,hold_action:{action:"none"},double_tap_action:{action:"none"},layout:"vertical",size:"40%",color_type:"icon",show_name:!0,show_state:!1,show_icon:!0,show_units:!0,show_label:!1,show_entity_picture:!1,show_live_stream:!1,card_size:3},i),{default_color:"DUMMY",color_off:"DUMMY",color_on:"DUMMY",lock:Object.assign({enabled:!1,duration:5,unlock:"tap"},i.lock)}),this._config.entity&&Ft.has(xe(this._config.entity))?this._config=Object.assign({tap_action:{action:"toggle"}},this._config):this._config=Object.assign({tap_action:{action:"more-info"}},this._config),this._config.default_color="var(--primary-text-color)","icon"!==this._config.color_type?this._config.color_off="var(--card-background-color)":this._config.color_off="var(--paper-item-icon-color)",this._config.color_on="var(--paper-item-icon-active-color)";const n=JSON.stringify(this._config);if(this._entities=[],Array.isArray(this._config.triggers_update)?this._entities=[...this._config.triggers_update]:"string"==typeof this._config.triggers_update&&"all"!==this._config.triggers_update&&this._entities.push(this._config.triggers_update),"all"!==this._config.triggers_update){const t=new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/,"gm"),e=new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/,"m"),i=n.match(t);null==i||i.forEach((t=>{const i=t.match(e);i&&!this._entities.includes(i[2])&&this._entities.push(i[2])}))}this._config.entity&&!this._entities.includes(this._config.entity)&&this._entities.push(this._config.entity),this._expandTriggerGroups();const r=new RegExp("\\[\\[\\[.*\\]\\]\\]","m");this._hasTemplate=!("all"!==this._config.triggers_update||!n.match(r)),this._initial_setup_complete||this._initConnected()}_loopGroup(t){t&&t.forEach((t=>{var e,i;(null===(e=this._hass)||void 0===e?void 0:e.states[t])&&("group"===xe(t)&&(null===(i=this._hass.states[t].attributes)||void 0===i?void 0:i.entity_id)?this._loopGroup(this._hass.states[t].attributes.entity_id):this._entities.includes(t)||this._entities.push(t))}))}_expandTriggerGroups(){var t;this._hass&&(null===(t=this._config)||void 0===t?void 0:t.group_expand)&&this._entities&&this._entities.forEach((t=>{var e,i;"group"===xe(t)&&this._loopGroup(null===(i=null===(e=this._hass)||void 0===e?void 0:e.states[t].attributes)||void 0===i?void 0:i.entity_id)}))}getCardSize(){var t;return(null===(t=this._config)||void 0===t?void 0:t.card_size)||3}_evalActions(t,e){const i=We(t),n=t=>t?(Object.keys(t).forEach((e=>{"object"==typeof t[e]?t[e]=n(t[e]):t[e]=this._getTemplateOrValue(this._stateObj,t[e])})),t):t;return i[e]=n(i[e]),!i[e].confirmation&&i.confirmation&&(i[e].confirmation=n(i.confirmation)),i}handleRippleActivate(t){this._ripple.then((e=>e&&e.startPress&&this._rippleHandlers.startPress(t)))}handleRippleDeactivate(){this._ripple.then((t=>t&&t.endPress&&this._rippleHandlers.endPress()))}handleRippleFocus(){this._ripple.then((t=>t&&t.startFocus&&this._rippleHandlers.startFocus()))}handleRippleBlur(){this._ripple.then((t=>t&&t.endFocus&&this._rippleHandlers.endFocus()))}_handleAction(t){var e;if(null===(e=t.detail)||void 0===e?void 0:e.action)switch(t.detail.action){case"tap":this._handleTap();break;case"hold":this._handleHold();break;case"double_tap":this._handleDblTap()}}_handleTap(){const t=this._config;t&&Gt(this,this._hass,this._evalActions(t,"tap_action"),!1,!1)}_handleHold(){const t=this._config;t&&Gt(this,this._hass,this._evalActions(t,"hold_action"),!0,!1)}_handleDblTap(){const t=this._config;t&&Gt(this,this._hass,this._evalActions(t,"double_tap_action"),!1,!0)}_handleUnlockType(t){const e=this._config;e&&e.lock.unlock===t.detail.action&&this._handleLock()}_handleLock(){const t=this.shadowRoot.getElementById("lock");if(!t)return;if(this._config.lock.exemptions){if(!this._hass.user.name||!this._hass.user.id)return;let e=!1;if(this._config.lock.exemptions.forEach((t=>{(!e&&t.user===this._hass.user.id||t.username===this._hass.user.name)&&(e=!0)})),!e)return t.classList.add("invalid"),void window.setTimeout((()=>{t&&t.classList.remove("invalid")}),3e3)}const e=this.shadowRoot.getElementById("overlay");if(e.style.setProperty("pointer-events","none"),t){const e=document.createAttribute("icon");e.value="mdi:lock-open-outline",t.attributes.setNamedItem(e),t.classList.add("hidden")}window.setTimeout((()=>{if(e.style.setProperty("pointer-events",""),t){t.classList.remove("hidden");const e=document.createAttribute("icon");e.value="mdi:lock-outline",t.attributes.setNamedItem(e)}}),1e3*this._config.lock.duration)}_stopPropagation(t){t.stopPropagation()}};var Xe,Ke,Qe;t([J()],Je.prototype,"_hass",void 0),t([J()],Je.prototype,"_config",void 0),t([J()],Je.prototype,"_timeRemaining",void 0),t([(Xe="mwc-ripple",(t,e)=>{const i={async get(){return await this.updateComplete,this.renderRoot.querySelector(Xe)},enumerable:!0,configurable:!0};return void 0!==e?X(i,t,e):K(i,t)})],Je.prototype,"_ripple",void 0),t([(Ke={passive:!0},(t,e)=>void 0!==e?((t,e,i)=>{Object.assign(e[i],t)})(Ke,t,e):((t,e)=>Object.assign(Object.assign({},e),{finisher(i){Object.assign(i.prototype[e.key],t)}}))(Ke,t))],Je.prototype,"handleRippleActivate",null),Je=t([(Qe="button-card",t=>"function"==typeof t?((t,e)=>(window.customElements.define(t,e),e))(Qe,t):((t,e)=>{const{kind:i,elements:n}=e;return{kind:i,elements:n,finisher(e){window.customElements.define(t,e)}}})(Qe,t))],Je); diff --git a/www/community/button-card/button-card.js.gz b/www/community/button-card/button-card.js.gz new file mode 100644 index 0000000..48f8103 Binary files /dev/null and b/www/community/button-card/button-card.js.gz differ diff --git a/www/community/custom-header/custom-header.js b/www/community/custom-header/custom-header.js deleted file mode 100644 index cde7d0d..0000000 --- a/www/community/custom-header/custom-header.js +++ /dev/null @@ -1,145 +0,0 @@ -function asyncGeneratorStep(e,t,n,r,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void n(e)}s.done?t(c):Promise.resolve(c).then(r,o)}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t3?0:(e-e%10!=10)*e%10]}};var c={D:function(e){return e.getDate()},DD:function(e){return pad(e.getDate())},Do:function(e,t){return t.DoFn(e.getDate())},d:function(e){return e.getDay()},dd:function(e){return pad(e.getDay())},ddd:function(e,t){return t.dayNamesShort[e.getDay()]},dddd:function(e,t){return t.dayNames[e.getDay()]},M:function(e){return e.getMonth()+1},MM:function(e){return pad(e.getMonth()+1)},MMM:function(e,t){return t.monthNamesShort[e.getMonth()]},MMMM:function(e,t){return t.monthNames[e.getMonth()]},YY:function(e){return pad(String(e.getFullYear()),4).substr(2)},YYYY:function(e){return pad(e.getFullYear(),4)},h:function(e){return e.getHours()%12||12},hh:function(e){return pad(e.getHours()%12||12)},H:function(e){return e.getHours()},HH:function(e){return pad(e.getHours())},m:function(e){return e.getMinutes()},mm:function(e){return pad(e.getMinutes())},s:function(e){return e.getSeconds()},ss:function(e){return pad(e.getSeconds())},S:function(e){return Math.round(e.getMilliseconds()/100)},SS:function(e){return pad(Math.round(e.getMilliseconds()/10),2)},SSS:function(e){return pad(e.getMilliseconds(),3)},a:function(e,t){return e.getHours()<12?t.amPm[0]:t.amPm[1]},A:function(e,t){return e.getHours()<12?t.amPm[0].toUpperCase():t.amPm[1].toUpperCase()},ZZ:function(e){var t=e.getTimezoneOffset();return(t>0?"-":"+")+pad(100*Math.floor(Math.abs(t)/60)+Math.abs(t)%60,4)}},l={D:["\\d\\d?",function(e,t){e.day=t}],Do:["\\d\\d?"+n,function(e,t){e.day=parseInt(t,10)}],M:["\\d\\d?",function(e,t){e.month=t-1}],YY:["\\d\\d?",function(e,t){var n=+(""+(new Date).getFullYear()).substr(0,2);e.year=""+(t>68?n-1:n)+t}],h:["\\d\\d?",function(e,t){e.hour=t}],m:["\\d\\d?",function(e,t){e.minute=t}],s:["\\d\\d?",function(e,t){e.second=t}],YYYY:["\\d{4}",function(e,t){e.year=t}],S:["\\d",function(e,t){e.millisecond=100*t}],SS:["\\d{2}",function(e,t){e.millisecond=10*t}],SSS:["\\d{3}",function(e,t){e.millisecond=t}],d:["\\d\\d?",noop],ddd:[n,noop],MMM:[n,monthUpdate("monthNamesShort")],MMMM:[n,monthUpdate("monthNames")],a:[n,function(e,t,n){var r=t.toLowerCase();r===n.amPm[0]?e.isPm=!1:r===n.amPm[1]&&(e.isPm=!0)}],ZZ:["[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z",function(e,t){var n,r=(t+"").match(/([+-]|\d\d)/gi);r&&(n=60*r[1]+parseInt(r[2],10),e.timezoneOffset="+"===r[0]?n:-n)}]};l.dd=l.d,l.dddd=l.ddd,l.DD=l.D,l.mm=l.m,l.hh=l.H=l.HH=l.h,l.MM=l.M,l.ss=l.s,l.A=l.a,e.masks={default:"ddd MMM DD YYYY HH:mm:ss",shortDate:"M/D/YY",mediumDate:"MMM D, YYYY",longDate:"MMMM D, YYYY",fullDate:"dddd, MMMM D, YYYY",shortTime:"HH:mm",mediumTime:"HH:mm:ss",longTime:"HH:mm:ss.SSS"},e.format=function(n,o,i){var a=i||e.i18n;if("number"==typeof n&&(n=new Date(n)),"[object Date]"!==Object.prototype.toString.call(n)||isNaN(n.getTime()))throw new Error("Invalid Date in fecha.format");o=e.masks[o]||o||e.masks.default;var s=[];return(o=(o=o.replace(r,(function(e,t){return s.push(t),"@@@"}))).replace(t,(function(e){return e in c?c[e](n,a):e.slice(1,e.length-1)}))).replace(/@@@/g,(function(){return s.shift()}))},e.parse=function(n,o,i){var a=i||e.i18n;if("string"!=typeof o)throw new Error("Invalid format in fecha.parse");if(o=e.masks[o]||o,n.length>1e3)return null;var s={},c=[],u=[],d=function regexEscape(e){return e.replace(/[|\\{()[^$+*?.-]/g,"\\$&")}(o=o.replace(r,(function(e,t){return u.push(t),"@@@"}))).replace(t,(function(e){if(l[e]){var t=l[e];return c.push(t[1]),"("+t[0]+")"}return e}));d=d.replace(/@@@/g,(function(){return u.shift()}));var h=n.match(new RegExp(d,"i"));if(!h)return null;for(var p=1;p1?"s":""," could not be found: ").concat(m.join(", ")));let g=(()=>{if(window.fully&&"function"==typeof fully.getDeviceId)return fully.getDeviceId();if(!localStorage["lovelace-player-device-id"]){const s4=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);localStorage["lovelace-player-device-id"]="".concat(s4()).concat(s4(),"-").concat(s4()).concat(s4())}return localStorage["lovelace-player-device-id"]})();Object.defineProperty(Array.prototype,"flat",{value:function value(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return this.reduce((function(t,n){return t.concat(Array.isArray(n)&&e-1?n.flat(e-1):n)}),[])}});const tapOrClick=(e,t)=>{e.addEventListener("click",()=>{t.dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1}))}),e.addEventListener("tap",()=>{t.dispatchEvent(new MouseEvent("tap",{bubbles:!1,cancelable:!1}))})},tabIndexByName=e=>{let t;const{views:n}=h.config;return isNaN(e)?n.forEach(r=>{r.title!==e&&r.path!==e||(t=n.indexOf(r))}):t=parseInt(e,10),t},subscribeRenderTemplate=(e,t,n)=>{const r=d.connection,o=_objectSpread2({user:d.user.name,browser:navigator.userAgent},t.variables,{},(e=>{const t=F(),n=new Date;if(t&&null!=t.config.views[t.current_view])return{deviceID:g,isAdmin:d.user.is_admin,isOwner:d.user.is_owner,user:d.user.name,userID:d.user.id,userAgent:navigator.userAgent,viewTitle:t.config.views[t.current_view].title,viewPath:t.config.views[t.current_view].path,viewIndex:t.current_view,url:window.location.href,time:n.toLocaleTimeString(e,{hour:"2-digit",minute:"2-digit"}),date:n.toLocaleDateString(e,{}),monthNum:n.getMonth()+1,monthNumLZ:String(n.getMonth()+1).padStart(2,0),monthNameShort:n.toLocaleDateString(e,{month:"short"}),monthNameLong:n.toLocaleDateString(e,{month:"long"}),dayNum:n.getDate(),dayNumLZ:String(n.getDate()).padStart(2,0),dayNameShort:n.toLocaleDateString(e,{weekday:"short"}),dayNameLong:n.toLocaleDateString(e,{weekday:"long"}),hours12:String((n.getHours()+24)%12||12),hours12LZ:String((n.getHours()+24)%12||12).padStart(2,0),hours24:n.getHours(),hours24LZ:String(n.getHours()).padStart(2,0),minutes:n.getMinutes(),minutesLZ:String(n.getMinutes()).padStart(2,0),year2d:String(n.getFullYear()).substr(-2),year4d:n.getFullYear(),AMPM:n.getHours()>=12?"PM":"AM",ampm:n.getHours()>=12?"pm":"am"}})(n)),i=t.template,a=t.entity_ids;return r.subscribeMessage(t=>e(t.result),{type:"render_template",template:i,variables:o,entity_ids:a})},buildRange=e=>{const t=[],range=(e,t)=>new Array(t-e+1).fill(void 0).map((t,n)=>n+e);if(e.includes("to")){const n=e.split("to");parseInt(n[1])>parseInt(n[0])?t.push(range(parseInt(n[0]),parseInt(n[1]))):t.push(range(parseInt(n[1]),parseInt(n[0])))}return t.flat()},processTabArray=e=>{let t=[];e="string"==typeof e?e.replace(/\s+/g,"").split(","):e;for(const n in e)if("string"==typeof e[n]&&e[n].includes("to")){const r=e[n].replace(/ /g,"").split("to");r[0].match(/[a-z]/g)||r[1].match(/[a-z]/g)?t.push(e[n]):t.push(buildRange(e[n]))}else t.push(e[n]);t=t.flat();for(const e in t)isNaN(t[e])?t[e]=tabIndexByName(t[e]):t[e]=parseInt(t[e]);return t.sort((e,t)=>e-t)},conditionalConfig=e=>{const t=F();let n={},r=0;return e.exceptions&&e.exceptions.forEach(e=>{const o=(e=>{const n={user:d.user.name,user_agent:navigator.userAgent,device_id:g};let r=0;for(const o in e)if(window.customHeaderTabCond=!!Object.keys(e).includes("tab"),"user"==o&&e[o].includes(","))e[o].split(/,+/).forEach(e=>{n[o]!=e.trim()&&e!=d.user.id||r++});else if("tab"==o&&(e[o].includes(",")||e[o].includes("to"))){const n=processTabArray(e[o]);if(t&&n.includes(t.current_view)){r++;continue}}else{if(!(n[o]==e[o]||"query_string"==o&&window.location.search.includes(e[o])||"user_agent"==o&&n[o].includes(e[o])||"media_query"==o&&window.matchMedia(e[o]).matches||"user"==o&&e[o]==d.user.id||"is_admin"==o&&e[o]==d.user.is_admin||"is_owner"==o&&e[o]==d.user.is_owner||"template"==o&&e[o]||t&&"tab"==o&&tabIndexByName(e[o])==t.current_view))return 0;r++}return r})(e.conditions);o>r&&(r=o,n=e.config)}),n.hide_tabs&&e.show_tabs&&n.hide_tabs.length&&e.show_tabs.length?delete e.show_tabs:n.show_tabs&&e.hide_tabs&&n.show_tabs.length&&e.hide_tabs.length&&delete e.hide_tabs,window.customHeaderExceptionConfig=JSON.stringify(n),_objectSpread2({},e,{},n)},_=(()=>{if(p.querySelector("ch-header"))return;const e={};e.bottom=document.createElement("ch-header-bottom"),e.tabContainer=document.createElement("paper-tabs"),e.tabContainer.setAttribute("scrollable",""),e.tabContainer.setAttribute("dir","ltr"),e.tabContainer.style.width="100%",e.tabContainer.style.marginLeft="0",f.tabs.forEach(t=>{const n=f.tabs.indexOf(t),r=t.cloneNode(!0),o=r.querySelector("ha-icon");o&&o.setAttribute("icon",h.config.views[n].icon),tapOrClick(r,f.tabs[n]),e.tabContainer.appendChild(r)}),e.tabs=e.tabContainer.querySelectorAll("paper-tab");const buildButton=(t,n)=>{if("options"===t){e[t]=f[t].cloneNode(!0),e[t].removeAttribute("horizontal-offset"),e[t].querySelector("paper-icon-button").style.height="48px";const n=Array.from(e[t].querySelectorAll("paper-item"));n.forEach(e=>{const r=n.indexOf(e);tapOrClick(e,f[t].querySelectorAll("paper-item")[r])})}else{if(!f[t])return;e[t]=document.createElement("paper-icon-button"),tapOrClick(e[t],f[t].shadowRoot.querySelector("paper-icon-button")||f[t])}e[t].setAttribute("icon",n),e[t].setAttribute("buttonElem",t),e[t].style.flexShrink="0",e[t].style.height="48px"};buildButton("menu","mdi:menu"),buildButton("voice","mdi:microphone"),buildButton("options","mdi:dots-vertical");const t=document.createElement("ch-stack"),n=document.createElement("div");return n.setAttribute("id","contentContainer"),e.container=document.createElement("ch-header"),e.menu&&e.container.appendChild(e.menu),e.container.appendChild(t),e.stack=e.container.querySelector("ch-stack"),e.stack.appendChild(n),e.stack.appendChild(e.tabContainer),e.voice&&"hidden"!=e.voice.style.visibility&&e.container.appendChild(e.voice),e.options&&e.container.appendChild(e.options),f.appLayout.appendChild(e.container),f.appLayout.appendChild(e.bottom),e})(),y=new WeakMap,isDirective=e=>"function"==typeof e&&y.has(e),v=void 0!==window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,removeNodes=(e,t,n=null)=>{for(;t!==n;){const n=t.nextSibling;e.removeChild(t),t=n}},b={},w={},S=`{{lit-${String(Math.random()).slice(2)}}}`,x=`\x3c!--${S}--\x3e`,E=new RegExp(`${S}|${x}`);class Template{constructor(e,t){this.parts=[],this.element=t;const n=[],r=[],o=document.createTreeWalker(t.content,133,null,!1);let i=0,a=-1,s=0;const{strings:c,values:{length:l}}=e;for(;s0;){const t=c[s],n=O.exec(t)[2],r=n.toLowerCase()+"$lit$",o=e.getAttribute(r);e.removeAttribute(r);const i=o.split(E);this.parts.push({type:"attribute",index:a,name:n,strings:i}),s+=i.length-1}}"TEMPLATE"===e.tagName&&(r.push(e),o.currentNode=e.content)}else if(3===e.nodeType){const t=e.data;if(t.indexOf(S)>=0){const r=e.parentNode,o=t.split(E),i=o.length-1;for(let t=0;t{const n=e.length-t.length;return n>=0&&e.slice(n)===t},isTemplatePartActive=e=>-1!==e.index,createMarker=()=>document.createComment(""),O=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/; -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */ -class TemplateInstance{constructor(e,t,n){this.__parts=[],this.template=e,this.processor=t,this.options=n}update(e){let t=0;for(const n of this.__parts)void 0!==n&&n.setValue(e[t]),t++;for(const e of this.__parts)void 0!==e&&e.commit()}_clone(){const e=v?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),t=[],n=this.template.parts,r=document.createTreeWalker(e,133,null,!1);let o,i=0,a=0,s=r.nextNode();for(;i-1||n)&&-1===e.indexOf("--\x3e",o+1);const i=O.exec(e);t+=null===i?e+(n?k:x):e.substr(0,i.index)+i[1]+i[2]+"$lit$"+i[3]+S}return t+=this.strings[e],t}getTemplateElement(){const e=document.createElement("template");return e.innerHTML=this.getHTML(),e}} -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */const isPrimitive=e=>null===e||!("object"==typeof e||"function"==typeof e),isIterable=e=>Array.isArray(e)||!(!e||!e[Symbol.iterator]);class AttributeCommitter{constructor(e,t,n){this.dirty=!0,this.element=e,this.name=t,this.strings=n,this.parts=[];for(let e=0;ethis.handleEvent(e)}setValue(e){this.__pendingValue=e}commit(){for(;isDirective(this.__pendingValue);){const e=this.__pendingValue;this.__pendingValue=b,e(this)}if(this.__pendingValue===b)return;const e=this.__pendingValue,t=this.value,n=null==e||null!=t&&(e.capture!==t.capture||e.once!==t.once||e.passive!==t.passive),r=null!=e&&(null==t||n);n&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),r&&(this.__options=getOptions(e),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=e,this.__pendingValue=b}handleEvent(e){"function"==typeof this.value?this.value.call(this.eventContext||this.element,e):this.value.handleEvent(e)}}const getOptions=e=>e&&(C?{capture:e.capture,passive:e.passive,once:e.once}:e.capture) -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */;const j=new class DefaultTemplateProcessor{handleAttributeExpressions(e,t,n,r){const o=t[0];if("."===o){return new PropertyCommitter(e,t.slice(1),n).parts}return"@"===o?[new EventPart(e,t.slice(1),r.eventContext)]:"?"===o?[new BooleanAttributePart(e,t.slice(1),n)]:new AttributeCommitter(e,t,n).parts}handleTextExpression(e){return new NodePart(e)}}; -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */function templateFactory(e){let t=P.get(e.type);void 0===t&&(t={stringsArray:new WeakMap,keyString:new Map},P.set(e.type,t));let n=t.stringsArray.get(e.strings);if(void 0!==n)return n;const r=e.strings.join(S);return n=t.keyString.get(r),void 0===n&&(n=new Template(e,e.getTemplateElement()),t.keyString.set(r,n)),t.stringsArray.set(e.strings,n),n}const P=new Map,M=new WeakMap; -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */ -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */ -(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.1.2");const html=(e,...t)=>new TemplateResult(e,t,"html",j) -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */;function removeNodesFromTemplate(e,t){const{element:{content:n},parts:r}=e,o=document.createTreeWalker(n,133,null,!1);let i=nextActiveIndexInTemplateParts(r),a=r[i],s=-1,c=0;const l=[];let u=null;for(;o.nextNode();){s++;const e=o.currentNode;for(e.previousSibling===u&&(u=null),t.has(e)&&(l.push(e),null===u&&(u=e)),null!==u&&c++;void 0!==a&&a.index===s;)a.index=null!==u?-1:a.index-c,i=nextActiveIndexInTemplateParts(r,i),a=r[i]}l.forEach(e=>e.parentNode.removeChild(e))}const countNodes=e=>{let t=11===e.nodeType?0:1;const n=document.createTreeWalker(e,133,null,!1);for(;n.nextNode();)t++;return t},nextActiveIndexInTemplateParts=(e,t=-1)=>{for(let n=t+1;n`${e}--${t}`;let T=!0;void 0===window.ShadyCSS?T=!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."),T=!1);const shadyTemplateFactory=e=>t=>{const n=getTemplateCacheKey(t.type,e);let r=P.get(n);void 0===r&&(r={stringsArray:new WeakMap,keyString:new Map},P.set(n,r));let o=r.stringsArray.get(t.strings);if(void 0!==o)return o;const i=t.strings.join(S);if(o=r.keyString.get(i),void 0===o){const n=t.getTemplateElement();T&&window.ShadyCSS.prepareTemplateDom(n,e),o=new Template(t,n),r.keyString.set(i,o)}return r.stringsArray.set(t.strings,o),o},R=["html","svg"],N=new Set,prepareTemplateStyles=(e,t,n)=>{N.add(e);const r=n?n.element:document.createElement("template"),o=t.querySelectorAll("style"),{length:i}=o;if(0===i)return void window.ShadyCSS.prepareTemplateStyles(r,e);const a=document.createElement("style");for(let e=0;e{R.forEach(t=>{const n=P.get(getTemplateCacheKey(t,e));void 0!==n&&n.keyString.forEach(e=>{const{element:{content:t}}=e,n=new Set;Array.from(t.querySelectorAll("style")).forEach(e=>{n.add(e)}),removeNodesFromTemplate(e,n)})})})(e);const s=r.content;n?function insertNodeIntoTemplate(e,t,n=null){const{element:{content:r},parts:o}=e;if(null==n)return void r.appendChild(t);const i=document.createTreeWalker(r,133,null,!1);let a=nextActiveIndexInTemplateParts(o),s=0,c=-1;for(;i.nextNode();){for(c++,i.currentNode===n&&(s=countNodes(t),n.parentNode.insertBefore(t,n));-1!==a&&o[a].index===c;){if(s>0){for(;-1!==a;)o[a].index+=s,a=nextActiveIndexInTemplateParts(o,a);return}a=nextActiveIndexInTemplateParts(o,a)}}}(n,a,s.firstChild):s.insertBefore(a,s.firstChild),window.ShadyCSS.prepareTemplateStyles(r,e);const c=s.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==c)t.insertBefore(c.cloneNode(!0),t.firstChild);else if(n){s.insertBefore(a,s.firstChild);const e=new Set;e.add(a),removeNodesFromTemplate(n,e)}};window.JSCompiler_renameProperty=(e,t)=>e;const q={toAttribute(e,t){switch(t){case Boolean:return e?"":null;case Object:case Array:return null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){switch(t){case Boolean:return null!==e;case Number:return null===e?null:Number(e);case Object:case Array:return JSON.parse(e)}return e}},notEqual=(e,t)=>t!==e&&(t==t||e==e),I={attribute:!0,type:String,converter:q,reflect:!1,hasChanged:notEqual},L=Promise.resolve(!0);class UpdatingElement extends HTMLElement{constructor(){super(),this._updateState=0,this._instanceProperties=void 0,this._updatePromise=L,this._hasConnectedResolver=void 0,this._changedProperties=new Map,this._reflectingProperties=void 0,this.initialize()}static get observedAttributes(){this.finalize();const e=[];return this._classProperties.forEach((t,n)=>{const r=this._attributeNameForProperty(n,t);void 0!==r&&(this._attributeToPropertyMap.set(r,n),e.push(r))}),e}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const e=Object.getPrototypeOf(this)._classProperties;void 0!==e&&e.forEach((e,t)=>this._classProperties.set(t,e))}}static createProperty(e,t=I){if(this._ensureClassProperties(),this._classProperties.set(e,t),t.noAccessor||this.prototype.hasOwnProperty(e))return;const n="symbol"==typeof e?Symbol():`__${e}`;Object.defineProperty(this.prototype,e,{get(){return this[n]},set(t){const r=this[e];this[n]=t,this._requestUpdate(e,r)},configurable:!0,enumerable:!0})}static finalize(){const e=Object.getPrototypeOf(this);if(e.hasOwnProperty("finalized")||e.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const e=this.properties,t=[...Object.getOwnPropertyNames(e),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e):[]];for(const n of t)this.createProperty(n,e[n])}}static _attributeNameForProperty(e,t){const n=t.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof e?e.toLowerCase():void 0}static _valueHasChanged(e,t,n=notEqual){return n(e,t)}static _propertyValueFromAttribute(e,t){const n=t.type,r=t.converter||q,o="function"==typeof r?r:r.fromAttribute;return o?o(e,n):e}static _propertyValueToAttribute(e,t){if(void 0===t.reflect)return;const n=t.type,r=t.converter;return(r&&r.toAttribute||q.toAttribute)(e,n)}initialize(){this._saveInstanceProperties(),this._requestUpdate()}_saveInstanceProperties(){this.constructor._classProperties.forEach((e,t)=>{if(this.hasOwnProperty(t)){const e=this[t];delete this[t],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(t,e)}})}_applyInstanceProperties(){this._instanceProperties.forEach((e,t)=>this[t]=e),this._instanceProperties=void 0}connectedCallback(){this._updateState=32|this._updateState,this._hasConnectedResolver&&(this._hasConnectedResolver(),this._hasConnectedResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(e,t,n){t!==n&&this._attributeToProperty(e,n)}_propertyToAttribute(e,t,n=I){const r=this.constructor,o=r._attributeNameForProperty(e,n);if(void 0!==o){const e=r._propertyValueToAttribute(t,n);if(void 0===e)return;this._updateState=8|this._updateState,null==e?this.removeAttribute(o):this.setAttribute(o,e),this._updateState=-9&this._updateState}}_attributeToProperty(e,t){if(8&this._updateState)return;const n=this.constructor,r=n._attributeToPropertyMap.get(e);if(void 0!==r){const e=n._classProperties.get(r)||I;this._updateState=16|this._updateState,this[r]=n._propertyValueFromAttribute(t,e),this._updateState=-17&this._updateState}}_requestUpdate(e,t){let n=!0;if(void 0!==e){const r=this.constructor,o=r._classProperties.get(e)||I;r._valueHasChanged(this[e],t,o.hasChanged)?(this._changedProperties.has(e)||this._changedProperties.set(e,t),!0!==o.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(e,o))):n=!1}!this._hasRequestedUpdate&&n&&this._enqueueUpdate()}requestUpdate(e,t){return this._requestUpdate(e,t),this.updateComplete}async _enqueueUpdate(){let e,t;this._updateState=4|this._updateState;const n=this._updatePromise;this._updatePromise=new Promise((n,r)=>{e=n,t=r});try{await n}catch(e){}this._hasConnected||await new Promise(e=>this._hasConnectedResolver=e);try{const e=this.performUpdate();null!=e&&await e}catch(e){t(e)}e(!this._hasRequestedUpdate)}get _hasConnected(){return 32&this._updateState}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){this._instanceProperties&&this._applyInstanceProperties();let e=!1;const t=this._changedProperties;try{e=this.shouldUpdate(t),e&&this.update(t)}catch(t){throw e=!1,t}finally{this._markUpdated()}e&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(t)),this.updated(t))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(e){return!0}update(e){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach((e,t)=>this._propertyToAttribute(t,this[t],e)),this._reflectingProperties=void 0)}updated(e){}firstUpdated(e){}}UpdatingElement.finalized=!0; -/** -@license -Copyright (c) 2019 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at -http://polymer.github.io/LICENSE.txt The complete set of authors may be found at -http://polymer.github.io/AUTHORS.txt The complete set of contributors may be -found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as -part of the polymer project is also subject to an additional IP rights grant -found at http://polymer.github.io/PATENTS.txt -*/ -const D="adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype; -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */(window.litElementVersions||(window.litElementVersions=[])).push("2.2.1");const flattenStyles=e=>e.flat?e.flat(1/0):function arrayFlat(e,t=[]){for(let n=0,r=e.length;n(e.add(t),e),new Set).forEach(e=>t.unshift(e))}else e&&t.push(e);return t}initialize(){super.initialize(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const e=this.constructor._styles;0!==e.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?D?this.renderRoot.adoptedStyleSheets=e.map(e=>e.styleSheet):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(e.map(e=>e.cssText),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(e){super.update(e);const t=this.render();t instanceof TemplateResult&&this.constructor.render(t,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach(e=>{const t=document.createElement("style");t.textContent=e.cssText,this.renderRoot.appendChild(t)}))}render(){}}LitElement.finalized=!0,LitElement.render=(e,t,n)=>{if(!n||"object"!=typeof n||!n.scopeName)throw new Error("The `scopeName` option is required.");const r=n.scopeName,o=M.has(t),i=T&&11===t.nodeType&&!!t.host,a=i&&!N.has(r),s=a?document.createDocumentFragment():t;if(((e,t,n)=>{let r=M.get(t);void 0===r&&(removeNodes(t,t.firstChild),M.set(t,r=new NodePart(Object.assign({templateFactory:templateFactory},n))),r.appendInto(t)),r.setValue(e),r.commit()})(e,s,Object.assign({templateFactory:shadyTemplateFactory(r)},n)),a){const e=M.get(s);M.delete(s);const n=e.value instanceof TemplateInstance?e.value.template:void 0;prepareTemplateStyles(r,s,n),removeNodes(t,t.firstChild),t.appendChild(s),M.set(t,e)}!o&&i&&window.ShadyCSS.styleElement(t.host)};const getThemeVar=e=>getComputedStyle(document.body).getPropertyValue(e),defaultConfig=()=>({locale:[],header_text:h.config.title||"Home Assistant",disabled_mode:!1,kiosk_mode:!1,compact_mode:!1,footer_mode:!1,split_mode:!1,disable_sidebar:!1,hide_header:!1,chevrons:!0,indicator_top:!1,hidden_tab_redirect:!0,shadow:!0,default_tab_on_refresh:!0,background:getThemeVar("--ch-background")||getThemeVar("--app-header-background-color")||"var(--primary-color)",elements_color:getThemeVar("--ch-elements-color")||getThemeVar("--app-header-text-color")||"var(--text-primary-color)",menu_color:getThemeVar("--ch-menu-color")||"",voice_color:getThemeVar("--ch-voice-color")||"",options_color:getThemeVar("--ch-options-color")||"",all_tabs_color:getThemeVar("--ch-all-tabs-color")||"",notification_dot_color:getThemeVar("--ch-notification-dot-color")||getThemeVar("var(--accent-color)")||"#ff9800",tab_indicator_color:getThemeVar("--ch-tab-indicator-color")||"",active_tab_color:getThemeVar("--ch-active-tab-color")||"",tabs_color:[],hide_tabs:[],show_tabs:[],default_tab:"",tab_icons:[],reverse_tab_direction:!1,button_icons:[],button_text:[],reverse_button_direction:!1,menu_dropdown:!1,menu_hide:!1,voice_dropdown:!1,voice_hide:!1,options_hide:!1,hide_help:!1,hide_unused:!1,hide_refresh:!1,hide_config:!1,hide_raw:!1,tabs_css:[],header_css:"",stack_css:"",header_text_css:"",active_tab_css:"",options_list_css:"",menu_css:"",options_css:"",voice_css:"",all_tabs_css:"",tab_container_css:"",view_css:"",panel_view_css:"",template_variables:"",exceptions:[],editor_warnings:!0});var z={version:"Version"},H={docs:"Docs",forums:"Forums"},V={add_exception:"Add Exception",automatic:"automatic",buttons:"Buttons",cancel:"Cancel",chevrons_tip:"Disable scrolling arrows for tabs.",chevrons_title:"Display Tab Chevrons",compact_mode_tip:"Make header compact.",compact_mode_title:"Compact Mode",conditions:"Conditions",config:"Config",current_user_agent:"Current User Agent",current_user:"Current User",default_tab:"Default tab",disable_sidebar_tip:"Disable sidebar and menu button.",disable_sidebar_title:"Disable Sidebar",disabled_mode_tip:"Completely disable Custom-Header.",disabled_mode_title:"Disabled Mode",disabled_template:"Disabled: The current value is a template.",editor_warnings_second_title:"Display Editor Warnings & Info",editor_warnings_tip:"Toggle editor warnings.",editor_warnings_title:"Display this info and warnings section.",exceptions:"Exceptions",footer_mode_tip:"Turn the header into a footer or reverse split mode.",footer_mode_title:"Footer Mode",header_text:"Header text.",hidden_tab_redirect_tip:"Redirect from hidden tab.",hidden_tab_redirect_title:"Hidden Tab Redirect",hide_configure_ui_tip:"Hide item in options menu.",hide_configure_ui_title:"Hide 'Configure UI'",hide_header_tip:"Hide header, but allow swipe out of sidebar.",hide_header_title:"Disable Header",hide_help_tip:"Hide item in options menu.",hide_help_title:"Hide 'Help'",hide_raw_editor_tip:"Hide item in options menu.",hide_raw_editor_title:"Hide 'Raw Config Editor'",hide_tab_list:"Comma-separated list of tab numbers to hide",hide_unused_tip:"Hide item in options menu.",hide_unused_title:"Hide 'Unused Entities'",kiosk_mode_tip:"Hide and disable the header and sidebar",kiosk_mode_title:"Kiosk Mode",locale_desc:"Locale for default template variables (date/time).",media_query:"Media Query",menu_dropdown_tip:"Put menu button in options menu.",menu_dropdown_title:"Menu in Dropdown Menu",menu_hide_tip:"Hide the menu button.",menu_hide_title:"Hide Menu Button",menu_items:"Menu Items",options_hide_tip:"Hide the options button.",options_hide_title:"Hide Options Button",query_string:"Query String",removes_edit_ui:"Removes ability to edit UI.",reverse_button_tip:"Reverses all buttons orientation.",reverse_button_title:"Reverse Buttons Orientation",reverse_tab_tip:"Places tabs on right side in reverse order.",reverse_tab_title:"Reverse Tab Direction",save_and_reload:"Save and reload",save_failed:"Save failed",shadow_tip:"Hide header shadows",shadow_title:"Shadows",show_tab_list:"Comma-separated list of tab numbers to show",split_mode_tip:"Split header into one header one footer",split_mode_title:"Split Mode",tabs_hide:"Hide Tabs",tabs_show:"Show Tabs",tabs:"Tabs",template:"Template",user_agent:"User Agent",user_list:"User (Seperate multiple users with a comma.)",voice_dropdown_tip:"Put voice button in options menu.",voice_dropdown_title:"Voice in Dropdown Menu",voice_hide_tip:"Hide the voice button.",voice_hide_title:"Hide Voice Button",warning_already_a_template:"Designates items that are already a template and won't be modified by the editor.",warning_disable_ch:"You can temporaily disable Custom-Header by adding '?disable_ch' to the end of your URL.",warning_edit_ui:"Marks items that remove your ability to edit the UI.",warning_jinja_info:"All text options accept Jinja. Hover over any item for more info.",warning_raw_editor_reload:"After using the 'Raw Config Editor' you need to reload the page to restore Custom Header."},U={common:z,links:H,editor:V},G={version:"Versjon"},B={docs:"Documentasjon",forums:"Forum"},W={add_exception:"Legg til unntak",automatic:"automatisk",buttons:"Brytere",cancel:"Avbryt",chevrons_tip:"Deaktiver pilene på tab båndet.",chevrons_title:"Vis tab piler",compact_mode_tip:"Gjør headeren compact.",compact_mode_title:"Kompakt modus",conditions:"Betingelser",config:"Konfigurasjon",current_user_agent:"Nåværende User Agent",current_user:"Pålogget bruker",default_tab:"Standard tab",disable_sidebar_tip:"Deaktiver side panelet og meny knappen.",disable_sidebar_title:"Deaktiver side panelet",disabled_mode_tip:"Deaktiver Custom-Header.",disabled_mode_title:"Deaktivert modus",disabled_template:"Deaktivert: Nåværende verdi er en mal.",editor_warnings_second_title:"Vis editor advarser of informasjon",editor_warnings_tip:"Veksle editor advarsler.",editor_warnings_title:"Vis denne informasjonen og advarsel seksjoner.",exceptions:"Unntak",footer_mode_tip:"Gjør header til footer eller reversert split modus.",footer_mode_title:"Footer modus",header_text:"Header tekst.",hidden_tab_redirect_tip:"Omadresser fra gjemt tab.",hidden_tab_redirect_title:"Gjemt tab omadresser",hide_configure_ui_tip:"Gjem 'Konfigurer brukergrensesnitt' in nedtrekksmenyen.",hide_configure_ui_title:"Gjem 'Konfigurer brukergrensesnitt'",hide_header_tip:"Gjem headderen, men tillat å dra inn fra siden for å åpne side panelet",hide_help_tip:"Deaktiver header",hide_help_title:"Gjem 'Hjelp'",hide_raw_editor_tip:"Gjem 'Tekstbasert konfigurasjonsredigering' in nedtrekksmenyen.",hide_raw_editor_title:"Gjem 'Tekstbasert konfigurasjonsredigering'",hide_tab_list:"Komma separert liste over tabber som skal gjemmes",hide_unused_tip:"Gjem 'Unused Entities' in nedtrekksmenyen.",hide_unused_title:"Gjem 'Unused Entities'",kiosk_mode_tip:"Gjem og deaktiver headeren og side panelet.",kiosk_mode_title:"Kiosk modus",locale_desc:"Locale for standard mal variabler (dato/tip).",menu_dropdown_tip:"Plasser meny i nedtrekksmenyen.",menu_dropdown_title:"Meny i nedtrekksmenyen",menu_hide_tip:"Gjem meny knappen.",menu_hide_title:"Gjem meny knappen",menu_items:"Meny elementer",options_hide_tip:"Gjem alternativer knappen.",options_hide_title:"Gjem alternativer knappen",removes_edit_ui:"Fjerner muligheten til å redigere brukergrensesnittet.",reverse_button_tip:"Reverser alle bryter orienteringer.",reverse_button_title:"Reverser bryter orientering",reverse_tab_tip:"Plasser tabber på høyre side i reversert rekkefølge.",reverse_tab_title:"Reverser tab rettning",save_and_reload:"Lagre og last inn på nytt",save_failed:"Lagring feilet",shadow_tip:"Gjemheader skygger",shadow_title:"Skygger",show_tab_list:"Komma separert liste over tabber som skal vises",split_mode_tip:"Split modus, både header og footer",split_mode_title:"Split modus",tabs_hide:"Gjem Tabber",tabs_show:"Vis Tabber",tabs:"Tabber",template:"mal",user_list:"Bruker (Skill flere brukere med komma.)",voice_dropdown_tip:"Plasser stemme i nedtrekksmenyen.",voice_dropdown_title:"Stemme i nedtrekksmenyen",voice_hide_tip:"Gjem stemme knappen.",voice_hide_title:"Gjem stemme knappen",warning_already_a_template:"Etter å ha brukt 'Tekstbasert konfigurasjonsredigering', må du laste inn siden på nytt for å gjenopprette Custom Header.",warning_disable_ch:"Du kan deaktivere Custom-Header midlertidig ved å legge til '?disable_ch' på slutten av nettadressen.",warning_edit_ui:"Markerer elementer som fjerner muligheten til å redigere brukergrensesnittet.",warning_jinja_info:"Alle tekstalternativer godtar Jinja. Hold musepekeren over et hvilket som helst element for mer informasjon.",warning_raw_editor_reload:"After using the 'Tekstbasert konfigurasjonsredigering' you need to reload the page to restore Custom Header."},Y={common:G,links:B,editor:W};const $={en:Object.freeze({__proto__:null,common:z,links:H,editor:V,default:U}),nb:Object.freeze({__proto__:null,common:G,links:B,editor:W,default:Y})};function localize(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";const r=e.split(".")[0],o=e.split(".")[1],i=(localStorage.getItem("selectedLanguage")||"en").replace(/['"]+/g,"").replace("-","_");let a;try{a=$[i][r][o]}catch(e){a=$.en[r][o]}return void 0===a&&(a=$.en[r][o]),""!==t&&""!==n&&(a=a.replace(t,n)),a}function _templateObject21(){const e=_taggedTemplateLiteral(['\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ']);return _templateObject21=function _templateObject21(){return e},e}function _templateObject20(){const e=_taggedTemplateLiteral([""]);return _templateObject20=function _templateObject20(){return e},e}function _templateObject19(){const e=_taggedTemplateLiteral(["\n \n "]);return _templateObject19=function _templateObject19(){return e},e}function _templateObject18(){const e=_taggedTemplateLiteral(["\n ",'\n \n \n \n \n
\n
\n ','\n \n \n \n \n
\n

','

\n \n \n

','

\n \n \n
\n
\n ']);return _templateObject18=function _templateObject18(){return e},e}function _templateObject17(){const e=_taggedTemplateLiteral([""]);return _templateObject17=function _templateObject17(){return e},e}function _templateObject16(){const e=_taggedTemplateLiteral(["\n \n "]);return _templateObject16=function _templateObject16(){return e},e}function _templateObject15(){const e=_taggedTemplateLiteral(['\n

Main Config

\n ']);return _templateObject15=function _templateObject15(){return e},e}function _templateObject14(){const e=_taggedTemplateLiteral(['\n
\n
\n

\n ','\n

\n

\n ','\n

\n
\n

\n \n ','\n
','
\n

\n
\n

\n ',"\n

\n
\n "]);return _templateObject14=function _templateObject14(){return e},e}function _templateObject13(){const e=_taggedTemplateLiteral(['\n

\n Custom Header\n

\n

\n \n \n ','   \n \n \n GitHub   \n \n \n ',"\n

\n ","\n ","\n "]);return _templateObject13=function _templateObject13(){return e},e}function _templateObject12(){const e=_taggedTemplateLiteral(['\n \n \n \n ',"\n ","\n ",'\n
\n ',"\n ","\n ","\n ","\n ","\n ","\n ","\n ","\n ","\n ","\n ",'\n
\n
\n
\n \n \n \n \n
\n

','

\n
\n ',"\n ","\n ","\n ",'\n
\n

','

\n
\n ',"\n ","\n ","\n ","\n ","\n ",'\n
\n

','

\n \n \n ',"\n ",'\n \n \n
\n
\n \n \n
\n
\n \n \n
\n \n \n ',"\n
\n "]);return _templateObject12=function _templateObject12(){return e},e}function _templateObject11(){const e=_taggedTemplateLiteral(['\n \n ']);return _templateObject10=function _templateObject10(){return e},e}function _templateObject9(){const e=_taggedTemplateLiteral(['\n \n ']);return _templateObject9=function _templateObject9(){return e},e}function _templateObject8(){const e=_taggedTemplateLiteral(["\n \n "]);return _templateObject8=function _templateObject8(){return e},e}function _templateObject7(){const e=_taggedTemplateLiteral(['\n ',"\n "]);return _templateObject7=function _templateObject7(){return e},e}function _templateObject6(){const e=_taggedTemplateLiteral(['\n ',"\n "]);return _templateObject6=function _templateObject6(){return e},e}function _templateObject5(){const e=_taggedTemplateLiteral(["\n ","\n "]);return _templateObject5=function _templateObject5(){return e},e}function _templateObject4(){const e=_taggedTemplateLiteral(["\n ","\n "]);return _templateObject4=function _templateObject4(){return e},e}function _templateObject3(){const e=_taggedTemplateLiteral(['\n \n \n ']);return _templateObject3=function _templateObject3(){return e},e}function _templateObject2(){const e=_taggedTemplateLiteral(['\n
\n X\n
\n ','\n \n \n

',"

\n
\n ",'\n
\n ','\n

',"

\n
\n Name: ","
\n ID: ",'\n

Device ID

\n
\n ','\n
\n

',"

\n
\n ",'\n
\n \n ',"\n ","\n \n "]);return _templateObject2=function _templateObject2(){return e},e}function _templateObject(){const e=_taggedTemplateLiteral([""]);return _templateObject=function _templateObject(){return e},e}customElements.define("custom-header-editor",class CustomHeaderEditor extends LitElement{static get properties(){return{_config:{}}}firstUpdated(){this.defaultConfig=defaultConfig(),this._lovelace=F(),this.deepcopy=this.deepcopy.bind(this),this._config=this._lovelace.config.custom_header?this.deepcopy(this._lovelace.config.custom_header):{}}render(){return this._config&&this._lovelace?html(_templateObject2(),this._close,this.renderStyle(),this.defaultConfig,this._config,this._configChanged,localize("editor.exceptions"),this._config.exceptions?this._config.exceptions.map((e,t)=>html(_templateObject3(),this._config,e,t,this._exceptionChanged,this._exceptionDelete)):"",this._addException,localize("editor.add_exception"),localize("editor.current_user"),d.user.name,d.user.id,g,localize("editor.current_user_agent"),navigator.userAgent,this.exception?"":html(_templateObject4(),this._save_button),this.exception?"":html(_templateObject5(),this._cancel_button)):html(_templateObject())}_close(){const e=this.parentNode.parentNode.parentNode.querySelector("editor");this.parentNode.parentNode.parentNode.removeChild(e)}_save(){for(const e in this._config)this._config[e]==this.defaultConfig[e]&&delete this._config[e];const e=_objectSpread2({},this._lovelace.config,{},{custom_header:this._config});try{this._lovelace.saveConfig(e).then(()=>{window.location.href=window.location.href})}catch(e){alert("".concat(localize("editor.save_failed"),": ").concat(e))}}get _save_button(){const e=localize("editor.save_and_reload");return html(_templateObject6(),this._save,e)}get _cancel_button(){return html(_templateObject7(),this._close,localize("editor.cancel"))}_addException(){let e;this._config.exceptions?(e=this._config.exceptions.slice(0),e.push({conditions:{},config:{}})):e=[{conditions:{},config:{}}],this._config=_objectSpread2({},this._config,{exceptions:e}),A(this,"config-changed",{config:this._config})}_configChanged(e){let{detail:t}=e;this._config&&(this._config=_objectSpread2({},this._config,{},t.config),A(this,"config-changed",{config:this._config}))}_exceptionChanged(e){if(!this._config)return;const t=e.target.index,n=this._config.exceptions.slice(0);n[t]=e.detail.exception;for(const e of n)for(const t in e.config)this._config[t]==e.config[t]?delete e.config[t]:this._config[t]||this.defaultConfig[t]!=e.config[t]||delete e.config[t];this._config=_objectSpread2({},this._config,{exceptions:n}),A(this,"config-changed",{config:this._config})}_exceptionDelete(e){if(!this._config)return;const t=e.target,n=this._config.exceptions.slice(0);n.splice(t.index,1),this._config=_objectSpread2({},this._config,{exceptions:n}),A(this,"config-changed",{config:this._config}),this.requestUpdate()}deepcopy(e){if(!e||"object"!=typeof e)return e;if("[object Date]"==Object.prototype.toString.call(e))return new Date(e.getTime());if(Array.isArray(e))return e.map(this.deepcopy);const t={};return Object.keys(e).forEach(n=>{t[n]=this.deepcopy(e[n])}),t}renderStyle(){return html(_templateObject8())}});customElements.define("ch-config-editor",class ChConfigEditor extends LitElement{static get properties(){return{defaultConfig:{},config:{},exception:{},_closed:{}}}constructor(){super(),this.buttonOptions=["show","hide","clock","overflow"],this.overflowOptions=["show","hide","clock"],this.swipeAnimation=["none","swipe","fade","flip"]}get _clock(){return"clock"==this.getConfig("menu")||"clock"==this.getConfig("voice")||"clock"==this.getConfig("options")}getConfig(e){return void 0!==this.config[e]?this.config[e]:this.defaultConfig[e]}templateExists(e){return!("string"!=typeof e||!e.includes("{{")&&!e.includes("{%"))}haSwitch(e,t,n,r,o,i){const a=html(_templateObject9(),localize("editor.disabled_template")),s=html(_templateObject10(),localize("editor.removes_edit_ui"));return html(_templateObject11(),this.exception&&void 0===this.config[e]?"inherited slotted":"slotted",!1!==this.getConfig(e)&&!this.templateExists(this.getConfig(e)),e,this._valueChanged,o,this.templateExists(this.getConfig(e)),r,this.templateExists(this.getConfig(e))&&t?a:"",n?s:"")}render(){return this.exception=void 0!==this.exception&&!1!==this.exception,html(_templateObject12(),this.exception?"":html(_templateObject13(),localize("links.docs"),localize("links.forums"),this.getConfig("editor_warnings")?html(_templateObject14(),localize("editor.warning_disable_ch"),localize("editor.warning_raw_editor_reload"),localize("editor.warning_already_a_template"),localize("editor.warning_edit_ui"),localize("editor.warning_jinja_info")):"",!this.exception&&this.getConfig("editor_warnings")?this.haSwitch("editor_warnings",!1,!1,localize("editor.editor_warnings_title"),localize("editor.editor_warnings_tip")):""),this.renderStyle(),this.getConfig("editor_warnings")&&!this.exception?html(_templateObject15()):"",this.haSwitch("disabled_mode",!0,!1,localize("editor.disabled_mode_title"),localize("editor.disabled_mode_tip")),this.haSwitch("footer_mode",!0,!1,localize("editor.footer_mode_title"),localize("editor.footer_mode_tip")),this.haSwitch("compact_mode",!0,!1,localize("editor.compact_mode_title"),localize("editor.compact_mode_tip")),this.haSwitch("split_mode",!0,!1,localize("editor.split_mode_title"),localize("editor.split_mode_tip")),this.haSwitch("kiosk_mode",!0,!0,localize("editor.kiosk_mode_title"),localize("editor.kiosk_mode_tip")),this.haSwitch("disable_sidebar",!0,!1,localize("editor.disable_sidebar_title"),localize("editor.disable_sidebar_tip")),this.haSwitch("hide_header",!0,!1,localize("editor.hide_header_title"),localize("editor.hide_header_tip")),this.haSwitch("chevrons",!0,!1,localize("editor.chevrons_title"),localize("editor.chevrons_tip")),this.haSwitch("hidden_tab_redirect",!0,!1,localize("editor.hidden_tab_redirect_title"),localize("editor.hidden_tab_redirect_tip")),this.haSwitch("shadow",!0,!1,localize("editor.shadow_title"),localize("editor.shadow_tip")),this.exception||this.getConfig("editor_warnings")?"":this.haSwitch("editor_warnings",!1,!1,localize("editor.editor_warnings_second_title"),localize("editor.editor_warnings_tip")),this.exception&&void 0===this.config.header_text?"inherited slotted":"slotted",localize("editor.header_text"),this.getConfig("header_text"),"header_text",this._valueChanged,localize("editor.automatic"),this.exception&&void 0===this.config.locale?"inherited slotted":"slotted",localize("editor.locale_desc"),this.getConfig("locale"),"locale",this._valueChanged,localize("editor.menu_items"),this.haSwitch("hide_config",!0,!0,localize("editor.hide_configure_ui_title"),localize("editor.hide_configure_ui_tip")),this.haSwitch("hide_raw",!0,!0,localize("editor.hide_raw_editor_title"),localize("editor.hide_raw_editor_tip")),this.haSwitch("hide_help",!0,!1,localize("editor.hide_help_title"),localize("editor.hide_help_tip")),this.haSwitch("hide_unused",!0,!1,localize("editor.hide_unused_title"),localize("editor.hide_unused_tip")),localize("editor.buttons"),this.haSwitch("menu_hide",!0,!1,localize("editor.menu_hide_title"),localize("editor.menu_hide_tip")),this.haSwitch("menu_dropdown",!0,!1,localize("editor.menu_dropdown_title"),localize("editor.menu_dropdown_tip")),this.haSwitch("voice_hide",!0,!1,localize("editor.voice_hide_title"),localize("editor.voice_hide_tip")),this.haSwitch("voice_dropdown",!0,!1,localize("editor.voice_dropdown_title"),localize("editor.voice_dropdown_tip")),this.haSwitch("options_hide",!0,!0,localize("editor.options_hide_title"),localize("editor.options_hide_tip")),this.haSwitch("reverse_button_direction",!0,!1,localize("editor.reverse_button_title"),localize("editor.reverse_button_tip")),localize("editor.tabs"),this._tabVisibility,this.getConfig("show_tabs").length>0?"1":"0",localize("editor.tabs_hide"),localize("editor.tabs_show"),this.getConfig("show_tabs").length>0?"initial":"none",this.exception&&void 0===this.config.show_tabs?"inherited slotted":"slotted",localize("editor.show_tab_list"),this.getConfig("show_tabs"),"show_tabs",this._valueChanged,this.getConfig("show_tabs").length>0?"none":"initial",this.exception&&void 0===this.config.hide_tabs?"inherited slotted":"slotted",localize("editor.hide_tab_list"),this.getConfig("hide_tabs"),"hide_tabs",this._valueChanged,this.exception&&void 0===this.config.default_tab?"inherited slotted":"slotted",localize("editor.default_tab"),this.getConfig("default_tab"),"default_tab",this._valueChanged,this.haSwitch("reverse_tab_direction",!0,!1,localize("editor.reverse_tab_title"),localize("editor.reverse_tab_tip")))}_toggleCard(){this._closed=!this._closed,A(this,"iron-resize")}_tabVisibility(){const e=this.shadowRoot.querySelector("#show"),t=this.shadowRoot.querySelector("#hide");"Hide Tabs"==this.shadowRoot.querySelector("#tabs").value?(e.style.display="none",t.style.display="initial"):(t.style.display="none",e.style.display="initial")}_valueChanged(e){this.config&&(e.target.configValue&&(""===e.target.value?delete this.config[e.target.configValue]:this.config=_objectSpread2({},this.config,{[e.target.configValue]:void 0!==e.target.checked?e.target.checked:e.target.value})),A(this,"ch-config-changed",{config:this.config}))}renderStyle(){return html(_templateObject16())}});customElements.define("ch-exception-editor",class ChExceptionEditor extends LitElement{static get properties(){return{config:{},exception:{},_closed:{}}}constructor(){super(),this._closed=!0}render(){return this.exception?html(_templateObject18(),this.renderStyle(),this._closed,Object.values(this.exception.conditions).join(", ").substring(0,40)||"New Exception",this._closed?"mdi:chevron-down":"mdi:chevron-up",this._toggleCard,this._closed,this._deleteException,localize("editor.conditions"),this.exception.conditions,this._conditionsChanged,localize("editor.config"),_objectSpread2({},this.defaultConfig,{},this.config),this.exception.config,this._configChanged):html(_templateObject17())}renderStyle(){return html(_templateObject19())}_toggleCard(){this._closed=!this._closed,A(this,"iron-resize")}_deleteException(){A(this,"ch-exception-delete")}_conditionsChanged(e){let{detail:t}=e;if(!this.exception)return;const n=_objectSpread2({},this.exception,{conditions:t.conditions});A(this,"ch-exception-changed",{exception:n})}_configChanged(e){if(e.stopPropagation(),!this.exception)return;const t=_objectSpread2({},this.exception,{config:e.detail.config});A(this,"ch-exception-changed",{exception:t})}});customElements.define("ch-conditions-editor",class ChConditionsEditor extends LitElement{static get properties(){return{conditions:{}}}get _user(){return this.conditions.user||""}get _user_agent(){return this.conditions.user_agent||""}get _media_query(){return this.conditions.media_query||""}get _query_string(){return this.conditions.query_string||""}get _template(){return this.conditions.template||""}get _tab(){return this.conditions.tab||""}get _device_id(){return this.conditions.device_id||""}render(){return this.conditions?html(_templateObject21(),localize("editor.user_list"),this._user,"user",this._valueChanged,localize("editor.user_agent"),this._user_agent,"user_agent",this._valueChanged,localize("editor.media_query"),this._media_query,"media_query",this._valueChanged,localize("editor.query_string"),this._query_string,"query_string",this._valueChanged,localize("editor.template"),this._template,"template",this._valueChanged,localize("editor.tabs"),this._tab,"tab",this._valueChanged,this._device_id,"device_id",this._valueChanged):html(_templateObject20())}_valueChanged(e){if(!this.conditions)return;const t=e.target;this["_".concat(t.configValue)]!==t.value&&(t.configValue&&(""===t.value?delete this.conditions[t.configValue]:this.conditions=_objectSpread2({},this.conditions,{[t.configValue]:t.value})),A(this,"ch-conditions-changed",{conditions:this.conditions}))}});const hideMenuItems=(e,t,n)=>{const localized=(e,t)=>{let n;const r=document.querySelector("home-assistant").hass;return n="raw_editor"===t?r.localize("ui.panel.lovelace.editor.menu.raw_editor"):"unused_entities"==t?r.localize("ui.panel.lovelace.unused_entities.title"):r.localize("ui.panel.lovelace.menu.".concat(t)),e.innerHTML.includes(n)||e.getAttribute("aria-label")==n};(n?document.querySelector("home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("app-toolbar > paper-menu-button"):t.options).querySelector("paper-listbox").querySelectorAll("paper-item").forEach(t=>{e.hide_help&&localized(t,"help")||e.hide_unused&&localized(t,"unused_entities")||e.hide_refresh&&localized(t,"refresh")||e.hide_config&&localized(t,"configure_ui")||e.hide_raw&&localized(t,"raw_editor")?t.style.display="none":t.style.display=""})},buttonToOverflow=(e,t,n,r)=>{n.options.querySelector("#".concat(e.toLowerCase(),"_dropdown"))&&n.options.querySelector("#".concat(e.toLowerCase(),"_dropdown")).remove();const o=document.createElement("paper-item"),i=document.createElement("ha-icon");o.setAttribute("id","".concat(e.toLowerCase(),"_dropdown")),i.setAttribute("icon",r.button_icons[e.toLowerCase()]||t),i.style.pointerEvents="none",r.reverse_button_direction?i.style.marginLeft="auto":i.style.marginRight="auto",o.innerText=e,o.appendChild(i),o.addEventListener("click",()=>{n[e.toLowerCase()].dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1}))}),i.addEventListener("click",()=>{n[e.toLowerCase()].dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1}))}),n.options.querySelector("paper-listbox").appendChild(o)},insertSettings=()=>{function insertAfter(e,t){t.parentNode.insertBefore(e,t.nextSibling)}if("storage"===h.mode){const e=document.createElement("paper-item");e.setAttribute("id","ch_settings"),e.addEventListener("click",()=>(()=>{if(window.scrollTo(0,0),!p.querySelector("ha-app-layout editor")){const e=document.createElement("editor"),t=document.createElement("div");t.style.cssText="\n padding: 20px;\n max-width: 600px;\n margin: 15px auto;\n background: var(--paper-card-background-color);\n border: 6px solid var(--paper-card-background-color);\n ",e.style.cssText="\n width: 100%;\n min-height: 100%;\n box-sizing: border-box;\n position: absolute;\n background: var(--background-color, grey);\n z-index: 100;\n padding: 5px;\n ",p.querySelector("ha-app-layout").insertBefore(e,p.querySelector("#view")),e.appendChild(t),t.appendChild(document.createElement("custom-header-editor"))}})()),e.innerHTML="Custom Header";const t=_.options.querySelector("paper-listbox").querySelectorAll("paper-item"),n=f.options.querySelector("paper-listbox").querySelectorAll("paper-item");_.options.querySelector("paper-listbox").querySelector("#ch_settings")||insertAfter(e,t[t.length-1]),f.options.querySelector("paper-listbox").querySelector("#ch_settings")||insertAfter(e,n[n.length-1])}},kioskMode=(e,t,n)=>{if(window.location.href.includes("disable_ch"))return;let r=document.createElement("style");r.setAttribute("id","ch_header_style"),t||(r.innerHTML+="\n #drawer {\n display: none;\n }\n "),e||(r.innerHTML+="\n ch-header {\n display: none;\n }\n ch-header-bottom {\n display: none;\n }\n app-header {\n display: none;\n }\n hui-view {\n padding-top: 100px;\n }\n hui-view, hui-panel-view {\n min-height: calc(100vh + 96px);\n margin-top: -96px;\n }\n ");const o=p.querySelector("#ch_header_style");o&&o.innerText==r.innerHTML||(p.appendChild(r),o&&o.remove()),n.hide_tabs&&n.hide_tabs.forEach(e=>{r.innerHTML+="\n paper-tab:nth-child(".concat(tabIndexByName(e)+1,") {\n display: none;\n }\n ")}),t||(f.drawer.style.display="none",f.sidebar.main.shadowRoot.querySelector("#ch_sidebar_style")||(r=document.createElement("style"),r.setAttribute("id","ch_sidebar_style"),r.innerHTML=":host(:not([expanded])) {width: 0px !important;}",f.sidebar.main.shadowRoot.appendChild(r)),f.main.shadowRoot.querySelector("#ch_sidebar_style")||(r=document.createElement("style"),r.setAttribute("id","ch_sidebar_style"),r.innerHTML=":host {--app-drawer-width: 0px !important;}",f.main.shadowRoot.appendChild(r))),window.dispatchEvent(new Event("resize"))},removeKioskMode=()=>{f.drawer.style.display="";let e=f.main.shadowRoot.querySelector("#ch_sidebar_style");e&&e.remove(),e=f.sidebar.main.shadowRoot.querySelector("#ch_sidebar_style"),e&&e.remove(),f.drawer.style.display=""},insertStyleTags=e=>{let t=48;const n=e.split_mode?_.bottom:_;e.compact_mode||(e.reverse_button_direction?(_.container.querySelector("#contentContainer").dir="ltr",_.container.querySelector("#contentContainer").style.textAlign="right"):(_.container.querySelector("#contentContainer").style.textAlign="",_.container.querySelector("#contentContainer").dir=""),_.container.querySelector("#contentContainer").innerHTML=e.header_text,t=n.tabs.length?96:48);let r=document.createElement("style");r.setAttribute("id","ch_header_style"),r.innerHTML="\n ch-header {\n padding-left: 10px;\n padding-right: 10px;\n box-sizing: border-box;\n display:flex;\n justify-content: center;\n font: 400 20px Roboto, sans-serif;\n background: ".concat(e.background||"var(--primary-color)",";\n color: ").concat(e.elements_color||"var(--text-primary-color)",";\n margin-bottom: 0px;\n margin-top: ").concat(e.footer_mode?"4px;":"0px",";\n position: sticky;\n position: -webkit-sticky;\n ").concat(e.footer_mode?"z-index: 99; bottom: 0px;":"top: 0px;","\n ").concat(e.header_css?e.header_css:"","\n transition: box-shadow 0.3s ease-in-out;\n ").concat(e.shadow&&(e.footer_mode||e.split_mode)?"box-shadow: 0px 0px 5px 5px rgba(0,0,0,0.2)":"","\n }\n ch-header-bottom {\n z-index: 99;\n padding-left: 10px;\n padding-right: 10px;\n box-sizing: border-box;\n display:flex;\n justify-content: center;\n font: 400 20px Roboto, sans-serif;\n background: ").concat(e.background||"var(--primary-color)",";\n color: ").concat(e.elements_color||"var(--text-primary-color)",";\n margin-bottom: 0px;\n margin-top: 0px;\n position: sticky;\n position: -webkit-sticky;\n ").concat(e.footer_mode?"top: 0px;":"bottom: 0px;","\n ").concat(e.header_css?e.header_css:"","\n ").concat(e.shadow?"box-shadow: 0px 0px 5px 5px rgba(0,0,0,0.2)":"","\n }\n ch-stack {\n flex-direction: column;\n width: 100%;\n margin-left: 9px;\n margin-right: 9px;\n ").concat(e.stack_css?e.stack_css:"","\n }\n ch-stack-bottom {\n flex-direction: column;\n width: 100%;\n margin-left: 9px;\n margin-right: 9px;\n ").concat(e.stack_css?e.stack_css:"","\n }\n #contentContainer {\n padding: 12px 6px 12px 6px;\n ").concat(e.compact_mode?"display: none;":"","\n ").concat(e.header_text.includes("
")?"\n font-size: 17px;\n line-height: 1.2;\n margin: -9px 0px 0px;\n ":"","\n ").concat(e.header_text_css?e.header_text_css:"","\n\n }\n app-header {\n display: none;\n }\n paper-tab.iron-selected {\n ").concat(e.active_tab_color?"color: ".concat(e.active_tab_color,";"):"","\n ").concat(e.active_tab_css?e.active_tab_css:"",'\n }\n [buttonElem="menu"] {\n ').concat(e.menu_color?"color: ".concat(e.menu_color,";"):"","\n ").concat(e.menu_hide?"display: none;":"","\n ").concat(e.menu_css?e.menu_css:"","\n ").concat(e.footer_mode&&e.compact_mode?"margin-top:0 !important;":"",'\n }\n [buttonElem="options"] {\n ').concat(e.options_color?"color: ".concat(e.options_color,";"):"","\n ").concat(e.options_hide?"display: none;":"","\n ").concat(e.options_css?e.options_css:"","\n ").concat(e.footer_mode&&e.compact_mode?"margin-top:0 !important;":"",'\n }\n [buttonElem="voice"] {\n ').concat(e.voice_color?"color: ".concat(e.voice_color,";"):"","\n ").concat(e.voice_hide?"display: none;":"","\n ").concat(e.voice_css?e.voice_css:"","\n ").concat(e.footer_mode&&e.compact_mode?"margin-top:0 !important;":"","\n }\n paper-tab {\n ").concat(e.all_tabs_color?"color: ".concat(e.all_tabs_color,";"):"","\n ").concat(e.all_tabs_css?e.all_tabs_css:"","\n }\n paper-tabs {\n ").concat(e.tab_indicator_color?"--paper-tabs-selection-bar-color: ".concat(e.tab_indicator_color," !important;"):"","\n ").concat(e.tab_container_css?e.tab_container_css:"","\n }\n "),e.tabs_color&&Object.keys(e.tabs_color).forEach(t=>{r.innerHTML+="\n paper-tab:nth-child(".concat(tabIndexByName(t)+1,") {\n color: ").concat(e.tabs_color[t],";\n }\n ")}),e.hide_tabs&&e.hide_tabs.forEach(e=>{r.innerHTML+="\n paper-tab:nth-child(".concat(tabIndexByName(e)+1,") {\n display: none;\n }\n ")}),e.tabs_css&&Object.keys(e.tabs_css).forEach(t=>{r.innerHTML+="\n paper-tab:nth-child(".concat(tabIndexByName(t)+1,") {\n ").concat(e.tabs_css[t],";\n }\n ")});let o=p.querySelector("#ch_header_style");p.appendChild(r),o&&o.remove(),r=document.createElement("style"),r.setAttribute("id","ch_view_style"),r.innerHTML="\n hui-view, hui-panel-view {\n margin-top: -96px;\n ".concat(e.compact_mode?"min-height: calc(100vh - 96px);":"min-height: calc(100vh - 112px);","\n ").concat(e.footer_mode&&!e.split_mode?"padding-bottom: ".concat(t,"px;"):"","\n ").concat(e.footer_mode&&!e.split_mode?"margin-bottom: -".concat(t+4,"px;"):"","\n ").concat(e.split_mode?"padding-bottom: 48px;":"","\n ").concat(e.split_mode&&e.footer_mode?"margin-bottom: -4px;":"","\n }\n hui-panel-view {\n padding-top: 96px;\n ").concat(e.panel_view_css?e.panel_view_css:"","\n ").concat(e.kiosk_mode||e.hide_header?"min-height: 100vh !important; margin-bottom: 0 !important;":"","\n }\n hui-view {\n padding-top: 100px;\n ").concat(e.view_css?e.view_css:"","\n }\n #view {\n min-height: calc(100vh - 96px) !important;\n ").concat(e.footer_mode&&!e.split_mode?"min-height: calc(100vh - ".concat(t,"px) !important;"):"","\n ").concat(e.compact_mode&&!e.footer_mode?"min-height: calc(100vh - ".concat(t,"px) !important;"):"","\n ").concat(e.split_mode?"min-height: calc(100vh - 48px) !important; margin-bottom: -48px;":"","\n ").concat(e.split_mode&&e.footer_mode?"min-height: calc(100vh - 52px) !important; margin-bottom: -48px;":"","\n ").concat(e.kiosk_mode||e.hide_header?"min-height: 100vh !important;":"","\n }\n "),o=p.querySelector("#ch_view_style"),o&&r.innerHTML==o.innerHTML||(p.appendChild(r),o&&o.remove()),r=document.createElement("style"),r.setAttribute("id","ch_chevron"),r.innerHTML='\n .not-visible[icon*="chevron"] {\n display:none;\n }\n ',o=n.tabContainer.shadowRoot.querySelector("#ch_chevron"),n.tabContainer.shadowRoot.appendChild(r),o&&o.remove(),o=_.bottom.querySelector("paper-tabs").shadowRoot.querySelector("#ch_chevron"),_.bottom.querySelector("paper-tabs").shadowRoot.appendChild(r.cloneNode(!0)),o&&o.remove(),r=document.createElement("style"),r.setAttribute("id","ch_animated"),r.innerHTML='\n ch-header, [buttonElem="menu"], [buttonElem="options"], [buttonElem="voice"] {\n transition: margin-top 0.4s ease-in-out;\n transition: top 0.4s ease-in-out;\n }\n ',setTimeout(()=>{p.querySelector("#ch_animated")||p.appendChild(r)},1e3)},redirects=(e,t)=>{const n=e.split_mode?t.bottom:t,r=f.sidebar.listbox.querySelector('[data-panel="lovelace"]');if(e.hide_tabs.includes(0)&&!e.default_tab){for(const e of n.tabs)if("none"!=getComputedStyle(e).display){r.setAttribute("href","/lovelace/".concat(n.tabContainer.indexOf(e)));break}}else e.default_tab&&r.setAttribute("href","/lovelace/".concat(tabIndexByName(e.default_tab)));const o=null!=e.default_tab?tabIndexByName(e.default_tab):null;if(e.hidden_tab_redirect&&f.tabs.length){const t=f.tabContainer.indexOf(f.tabContainer.querySelector("paper-tab.iron-selected"));if(e.hide_tabs.includes(t)&&e.hide_tabs.length!=f.tabs.length)if(o&&!e.hide_tabs.includes(tabIndexByName(o)))"none"!=getComputedStyle(n.tabs[o]).display&&(f.tabs[o].dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1})),r.setAttribute("href","/lovelace/".concat(o)));else for(const e of n.tabs)if("none"!=getComputedStyle(e).display){e.dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1})),r.setAttribute("href","/lovelace/".concat(n.tabContainer.indexOf(e)));break}}let i;i=!e.default_tab_on_refresh&&window.performance?0==performance.navigation.type:!(e.default_tab_on_refresh||!performance.getEntriesByType("navigation"))&&"reload"==performance.getEntriesByType("navigation")[0].type,i||null==o||window.customHeaderDefaultClicked||!f.tabs[o]||(f.tabs[o].dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1})),window.customHeaderDefaultClicked=!0)},styleHeader=e=>{if(insertSettings(),window.location.href.includes("disable_ch")&&(e.disabled_mode=!0),e.disabled_mode)return window.customHeaderDisabled=!0,removeKioskMode(),_.container&&(_.container.style.visibility="hidden"),p.querySelector("#ch_header_style")&&p.querySelector("#ch_header_style").remove(),p.querySelector("#ch_view_style")&&p.querySelector("#ch_view_style").remove(),_.tabContainer.shadowRoot.querySelector("#ch_chevron")&&(_.tabContainer.shadowRoot.querySelector("#ch_chevron").remove(),_.bottom.tabContainer.shadowRoot.querySelector("#ch_chevron").remove()),_.menu.style.display="none",p.querySelector("ha-menu-button").style.display="",f.sidebar.main.shadowRoot.querySelector(".menu").style="",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="",f.sidebar.main.shadowRoot.querySelector("div.divider").style="",void window.dispatchEvent(new Event("resize"));window.customHeaderDisabled=!1,hideMenuItems(e,_,!1),_.menu.style.display="",_.container&&(_.container.style.visibility="visible"),insertSettings(),p.querySelector("ch-header-bottom").querySelector("paper-tabs")||(_.bottom.appendChild(_.tabContainer.cloneNode(!0)),_.bottom.tabContainer=p.querySelector("ch-header-bottom").querySelector("paper-tabs"),_.bottom.tabs=_.bottom.tabContainer.querySelectorAll("paper-tab")),f.tabs.forEach(e=>{const t=f.tabs.indexOf(e);_.bottom.tabs[t].addEventListener("click",()=>{f.tabs[t].dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1}))})}),e.split_mode?(_.tabContainer.style.display="none",e.compact_mode=!1,e.footer_mode?_.bottom.setAttribute("slot","header"):_.bottom.setAttribute("slot","")):_.bottom.style.display="none";const t=e.split_mode?_.bottom:_;e.split_mode||(_.bottom.style.display="none"),_.tabs.length||(e.compact_mode=!1),e.menu_dropdown&&!e.disable_sidebar?buttonToOverflow("Menu","mdi:menu",_,e):_.options.querySelector("#menu_dropdown")&&_.options.querySelector("#menu_dropdown").remove(),e.voice_dropdown?buttonToOverflow("Voice","mdi:microphone",_,e):_.options.querySelector("#voice_dropdown")&&_.options.querySelector("#voice_dropdown").remove(),e.reverse_button_direction?(_.options.setAttribute("horizontal-align","left"),_.options.querySelector("paper-listbox").setAttribute("dir","ltr")):(_.options.setAttribute("horizontal-align","right"),_.options.querySelector("paper-listbox").setAttribute("dir","rtl"));const n=document.createElement("style");if(n.setAttribute("id","ch_header_style"),n.innerHTML="\n .menu, paper-listbox {\n transition: height 0.1s ease-in-out 0s;\n }\n .divider {\n transition: margin-bottom 0.1s ease-in-out 0s;\n }\n ",f.sidebar.main.shadowRoot.appendChild(n),e.kiosk_mode&&!e.disabled_mode?(insertStyleTags(e),kioskMode(!1,!1,e)):e.disable_sidebar?(kioskMode(!0,!1,e),insertStyleTags(e)):e.hide_header?(insertStyleTags(e),kioskMode(!1,!0,e)):e.disable_sidebar||e.kiosk_mode||e.hide_header||(removeKioskMode(),e.compact_mode&&!e.footer_mode?(f.sidebar.main.shadowRoot.querySelector(".menu").style="height:49px;",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="height:calc(100% - 175px);",f.sidebar.main.shadowRoot.querySelector("div.divider").style=""):e.footer_mode&&!e.split_mode?(f.sidebar.main.shadowRoot.querySelector(".menu").style="",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="height: calc(100% - 170px);",f.sidebar.main.shadowRoot.querySelector("div.divider").style="margin-bottom: -10px;"):e.split_mode?(f.sidebar.main.shadowRoot.querySelector(".menu").style="height:49px;",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="height: calc(100% - 170px);",f.sidebar.main.shadowRoot.querySelector("div.divider").style="margin-bottom: -3px;"):(f.sidebar.main.shadowRoot.querySelector(".menu").style="",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="",f.sidebar.main.shadowRoot.querySelector("div.divider").style=""),insertStyleTags(e)),e.chevrons?t.tabContainer.hideScrollButtons=!1:t.tabContainer.hideScrollButtons=!0,e.indicator_top?t.tabContainer.alignBottom=!0:t.tabContainer.alignBottom=!1,e.footer_mode?_.options.setAttribute("vertical-align","bottom"):_.options.removeAttribute("vertical-align"),e.footer_mode?_.container.removeAttribute("slot"):_.container.setAttribute("slot","header"),t.tabContainer.dir=e.reverse_tab_direction?"rtl":"ltr",_.container.dir=e.reverse_button_direction?"rtl":"ltr",e.tab_icons&&t.tabs.length)for(const n in e.tab_icons){const r=tabIndexByName(n),o=t.tabs[r].querySelector("ha-icon");e.tab_icons[n]?o.icon=e.tab_icons[n]:o.icon=h.config.views[r].icon}if(e.button_icons)for(const t in e.button_icons)_[t]&&(e.button_icons[t]?"options"===t?_[t].querySelector("paper-icon-button").icon=e.button_icons[t]:_[t].icon=e.button_icons[t]:"menu"===t?_.menu.icon="mdi:menu":"voice"===t&&_.voice?_.voice.icon="mdi:microphone":"options"===t&&(_[t].querySelector("paper-icon-button").icon="mdi:dots-vertical"));if(e.button_text)for(const t in e.button_text){const n=document.createElement("p");n.className="buttonText";const r="options"===t?_[t].querySelector("paper-icon-button"):_[t];e.button_text[t]||!r.shadowRoot.querySelector(".buttonText")?e.button_text[t]&&(r.shadowRoot.querySelector(".buttonText")?r.shadowRoot.querySelector(".buttonText").innerHTML=e.button_text[t]:(n.innerHTML=e.button_text[t],r.shadowRoot.appendChild(n)),r.shadowRoot.querySelector(".buttonText").dir="ltr",r.shadowRoot.querySelector(".buttonText").style.textAlign="menu"==t?e.reverse_button_direction?"right":"left":e.reverse_button_direction?"left":"right",r.shadowRoot.querySelector("iron-icon").style.display="none",r.style.width="auto",e.button_text[t].includes("
")?(r.shadowRoot.querySelector(".buttonText").style.fontSize="17px",r.shadowRoot.querySelector(".buttonText").style.lineHeight="1.2",r.shadowRoot.querySelector(".buttonText").style.margin="-5px 0px 0px 0px"):r.shadowRoot.querySelector(".buttonText").style.margin="5.5px 0px 0px 0px"):(r.shadowRoot.querySelector(".buttonText").remove(),r.shadowRoot.querySelector("iron-icon").style.display="",r.style.width="")}t.tabs.length||(t.tabContainer.style.display="none"),((e,t)=>{if(e.menu_hide)return;const buildDot=()=>{const t=document.createElement("div");return t.className="dot",t.style.cssText="\n pointer-events: none;\n position: relative;\n background-color: ".concat(e.notification_dot_color,";\n width: 12px;\n height: 12px;\n top: -28px;\n right: ").concat(e.reverse_button_direction?"":"-","16px;\n border-radius: 50%;\n "),t},menuButtonVisibility=()=>{e.disable_sidebar||window.customHeaderDisabled||("hidden"===f.menu.style.visibility?t.menu.style.display="none":t.menu.style.display="initial")},notificationDot=e=>{const t=document.querySelector("home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-panel-lovelace").shadowRoot.querySelector("hui-root");e.forEach(e=>{let{addedNodes:n,removedNodes:r}=e;if(n)for(const e of n)"dot"!==e.className||t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".dot")||(t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.appendChild(buildDot()),t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".buttonText")&&(t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".dot").style.display="none",t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".buttonText").style.borderBottom="3px solid ".concat(window.customHeaderConfig.notification_dot_color)));if(r)for(const e of r)"dot"===e.className&&t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".dot")&&(t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".dot").remove(),t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".buttonText")&&(t.shadowRoot.querySelector('[buttonElem="menu"]').shadowRoot.querySelector(".buttonText").style.borderBottom=""))})};if(!window.customHeaderMenuObserver){window.customHeaderMenuObserver=!0,new MutationObserver(notificationDot).observe(f.menu.shadowRoot,{childList:!0}),new MutationObserver(menuButtonVisibility).observe(f.menu,{attributes:!0,attributeFilter:["style"]})}menuButtonVisibility();const n=t.menu.shadowRoot.querySelector(".dot");n&&n.style.cssText!=buildDot().style.cssText&&(n.remove(),e.button_text.menu&&(t.menu.shadowRoot.querySelector(".buttonText").style.textDecoration="")),!t.menu.shadowRoot.querySelector(".dot")&&f.menu.shadowRoot.querySelector(".dot")&&(t.menu.shadowRoot.appendChild(buildDot()),e.button_text.menu&&(t.menu.shadowRoot.querySelector(".dot").style.display="none",t.menu.shadowRoot.querySelector(".buttonText").style.borderBottom="3px solid ".concat(e.notification_dot_color)))})(e,_),window.customHeaderShrink||e.split_mode||(window.customHeaderShrink=!0,window.addEventListener("scroll",(function(e){const t="none"===window.getComputedStyle(_.container.querySelector("#contentContainer")).getPropertyValue("display");"0px"===window.getComputedStyle(_.container).getPropertyValue("bottom")||t||(window.scrollY>48?(_.container.style.top="-48px",_.menu.style.marginTop="48px",_.voice&&(_.voice.style.marginTop="48px"),_.options.style.marginTop="48px"):(_.container.style.transition="0s",_.menu.style.transition="0s",_.voice&&(_.voice.style.transition="0s"),_.options.style.transition="0s",_.container.style.top="-".concat(window.scrollY,"px"),_.menu.style.marginTop="".concat(window.scrollY,"px"),_.voice&&(_.voice.style.marginTop="".concat(window.scrollY,"px")),_.options.style.marginTop="".concat(window.scrollY,"px")),_.container.style.transition="")})));const shadowScroll=()=>{window.scrollY>15?_.container.style.boxShadow="0px 0px 5px 5px rgba(0,0,0,0.2)":_.container.style.boxShadow="0px 0px 5px 5px rgba(0,0,0,0)"};window.customHeaderShadow||e.footer_mode||e.split_mode||!e.shadow?window.removeEventListener("scroll",shadowScroll):(window.customHeaderShadow=!0,window.addEventListener("scroll",shadowScroll)),A(_.container,"iron-resize"),redirects(e,_)},observers=()=>{const e=new MutationObserver(e=>{const t=window.customHeaderConfig,n=t.split_mode?_.bottom:_;e.forEach(r=>{let{addedNodes:o,target:i}=r;e.length&&"HTML"==e[0].target.nodeName&&(window.customHeaderExceptionConfig="init",buildConfig(),e=[]),"view"==i.id&&o.length&&n.tabs.length?(redirects(t,_),(e=>{const t=e.split_mode?_.bottom:_,n=F();n&&(f.tabContainer||t.tabContainer)&&(t.tabs[n.current_view].dispatchEvent(new MouseEvent("click",{bubbles:!1,cancelable:!1})),A(_.container,"iron-resize"))})(t),window.customHeaderTabCond&&buildConfig()):o.length&&"PARTIAL-PANEL-RESOLVER"==i.nodeName?(f.main.shadowRoot.querySelector(" ha-panel-lovelace")?t.compact_mode&&!t.footer_mode?(f.sidebar.main.shadowRoot.querySelector(".menu").style="height:49px;",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="height:calc(100% - 175px);",f.sidebar.main.shadowRoot.querySelector("div.divider").style=""):t.footer_mode&&!t.split_mode?(f.sidebar.main.shadowRoot.querySelector(".menu").style="",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="height: calc(100% - 170px);",f.sidebar.main.shadowRoot.querySelector("div.divider").style="margin-bottom: -10px;"):t.split_mode&&(f.sidebar.main.shadowRoot.querySelector(".menu").style="height:49px;",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="height: calc(100% - 170px);",f.sidebar.main.shadowRoot.querySelector("div.divider").style="margin-bottom: -3px;"):(f.sidebar.main.shadowRoot.querySelector(".menu").style="",f.sidebar.main.shadowRoot.querySelector("paper-listbox").style="",f.sidebar.main.shadowRoot.querySelector("div.divider").style=""),p.querySelector("editor")&&p.querySelector("editor").remove()):"edit-mode"===i.className&&o.length?(p.querySelector("editor")&&p.querySelector("editor").remove(),window.customHeaderDisabled||hideMenuItems(t,_,!0),_.menu.style.display="none",p.querySelector("ch-header").style.display="none",f.appHeader.style.display="block",p.querySelector("#ch_view_style")&&p.querySelector("#ch_view_style").remove(),p.querySelector("#ch_header_style")&&p.querySelector("#ch_header_style").remove(),p.querySelector("#ch_animated")&&p.querySelector("#ch_animated").remove()):"APP-HEADER"===i.nodeName&&o.length&&(insertStyleTags(t),f.menu=f.appHeader.querySelector("ha-menu-button"),f.appHeader.style.display="none",_.menu.style.display="",p.querySelector("ch-header").style.display="")})});e.observe(f.partialPanelResolver,{childList:!0}),e.observe(f.appHeader,{childList:!0}),e.observe(p.querySelector("#view"),{childList:!0}),e.observe(document.querySelector("html"),{attributes:!0})};window.customHeaderExceptionConfig="init";const buildConfig=e=>{const t=defaultConfig();e||(e=_objectSpread2({},t,{},h.config.custom_header));const n=_objectSpread2({},e,{},conditionalConfig(e)),r=e.template_variables;delete e.template_variables;const o="boolean"==typeof n.disabled_mode&&n.disabled_mode||"string"==typeof n.disabled_mode&&!n.disabled_mode.includes("{{")&&!n.disabled_mode.includes("{%")||window.location.href.includes("disable_ch"),processAndContinue=()=>{(e=_objectSpread2({},e,{},conditionalConfig(e))).hide_tabs&&(e.hide_tabs=processTabArray(e.hide_tabs)),e.show_tabs&&(e.show_tabs=processTabArray(e.show_tabs)),e.show_tabs&&e.show_tabs.length&&(e.hide_tabs=(e=>{if(e&&e.length){const t=[];for(let e=0;e!e.includes(t))}})(e.show_tabs)),(e.disable_sidebar||e.menu_dropdown)&&(e.menu_hide=!0),e.voice_dropdown&&(e.voice_hide=!0),null!=e.header_text&&""==e.header_text&&(e.header_text=t.header_text),e.header_text&&" "==e.header_text&&(e.header_text=" "),e.hide_header&&e.disable_sidebar&&(e.kiosk_mode=!0,e.hide_header=!1,e.disable_sidebar=!1),null!=e.test_template&&(o?console.log("Custom Header cannot render templates while disabled."):"string"==typeof e.test_template&&(e.test_template.toLowerCase().includes("true")||e.test_template.toLowerCase().includes("false"))?(console.log('Custom Header test returned: "'.concat(e.test_template,'"')),console.log("Warning: Boolean is returned as string instead of Boolean.")):"string"==typeof e.test_template?console.log('Custom Header test returned: "'.concat(e.test_template,'"')):console.log("Custom Header test returned: ".concat(e.test_template))),window.customHeaderConfig=e,styleHeader(e),window.customHeaderObservers||(observers(),window.customHeaderObservers=!0)},i=JSON.stringify(e),a=!!r||i.includes("{{")||i.includes("{%");let s,c=!1;a&&!o?("function"==typeof window.unsub&&(window.unsub(),window.unsub=null),s=subscribeRenderTemplate(t=>{if(window.customHeaderLastTemplateResult!=t||window.customHeaderLastExceptionConfig!=window.customHeaderExceptionConfig){window.customHeaderLastExceptionConfig=window.customHeaderExceptionConfig,window.customHeaderLastTemplateResult=t;try{e=JSON.parse(t.replace(/"true"/gi,"true").replace(/"false"/gi,"false").replace(/""/,""))}catch(e){c=!0;let n="[CUSTOM-HEADER] There was an issue with the template: ".concat(((e,t)=>{const n=t.toString().match(/\d+/g)[0],r=e.substr(0,n).match(/[^,]*$/),o=e.substr(n).match(/^[^,]*/);return"".concat(r?r[0]:"").concat(o?o[0]:"").replace('":"',': "')})(t,e));n.includes("locale")&&(n='[CUSTOM-HEADER] There was an issue one of your "template_variables".'),console.log(n)}processAndContinue()}},{template:JSON.stringify(r).replace(/\\/g,"")+JSON.stringify(e).replace(/\\/g,"")},e.locale),function _asyncToGenerator(e){return function(){var t=this,n=arguments;return new Promise((function(r,o){var i=e.apply(t,n);function _next(e){asyncGeneratorStep(i,r,o,_next,_throw,"next",e)}function _throw(e){asyncGeneratorStep(i,r,o,_next,_throw,"throw",e)}_next(void 0)}))}}((function*(){try{window.unsub=yield s}catch(e){c=!0,console.log("[CUSTOM-HEADER] There was an error with one or more of your templates:"),console.log("".concat(e.message.substring(0,e.message.indexOf(")")),")"))}}))(),clearTimeout(window.customHeaderTempTimeout),window.customHeaderTempTimeout=window.setTimeout(()=>{const e=document.querySelector("home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-panel-lovelace");(e?e.shadowRoot.querySelector("hui-editor"):null)||c||p.querySelector("custom-header-editor")||buildConfig()},1e3*(60-(new Date).getSeconds()))):processAndContinue()};function createCommonjsModule(e,t){return e(t={exports:{}},t.exports),t.exports}var J=createCommonjsModule((function(e){var t=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=t)})),K={}.hasOwnProperty,_has=function(e,t){return K.call(e,t)},_fails=function(e){try{return!!e()}catch(e){return!0}},Z=!_fails((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})),X=createCommonjsModule((function(e){var t=e.exports={version:"2.6.11"};"number"==typeof __e&&(__e=t)})),Q=(X.version,function(e){return"object"==typeof e?null!==e:"function"==typeof e}),_anObject=function(e){if(!Q(e))throw TypeError(e+" is not an object!");return e},ee=J.document,te=Q(ee)&&Q(ee.createElement),_domCreate=function(e){return te?ee.createElement(e):{}},ne=!Z&&!_fails((function(){return 7!=Object.defineProperty(_domCreate("div"),"a",{get:function(){return 7}}).a})),_toPrimitive=function(e,t){if(!Q(e))return e;var n,r;if(t&&"function"==typeof(n=e.toString)&&!Q(r=n.call(e)))return r;if("function"==typeof(n=e.valueOf)&&!Q(r=n.call(e)))return r;if(!t&&"function"==typeof(n=e.toString)&&!Q(r=n.call(e)))return r;throw TypeError("Can't convert object to primitive value")},re=Object.defineProperty,oe={f:Z?Object.defineProperty:function defineProperty(e,t,n){if(_anObject(e),t=_toPrimitive(t,!0),_anObject(n),ne)try{return re(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},_propertyDesc=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},ie=Z?function(e,t,n){return oe.f(e,t,_propertyDesc(1,n))}:function(e,t,n){return e[t]=n,e},ae=0,se=Math.random(),_uid=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++ae+se).toString(36))},ce=createCommonjsModule((function(e){var t=J["__core-js_shared__"]||(J["__core-js_shared__"]={});(e.exports=function(e,n){return t[e]||(t[e]=void 0!==n?n:{})})("versions",[]).push({version:X.version,mode:"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})})),le=ce("native-function-to-string",Function.toString),ue=createCommonjsModule((function(e){var t=_uid("src"),n=(""+le).split("toString");X.inspectSource=function(e){return le.call(e)},(e.exports=function(e,r,o,i){var a="function"==typeof o;a&&(_has(o,"name")||ie(o,"name",r)),e[r]!==o&&(a&&(_has(o,t)||ie(o,t,e[r]?""+e[r]:n.join(String(r)))),e===J?e[r]=o:i?e[r]?e[r]=o:ie(e,r,o):(delete e[r],ie(e,r,o)))})(Function.prototype,"toString",(function toString(){return"function"==typeof this&&this[t]||le.call(this)}))})),_aFunction=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e},_ctx=function(e,t,n){if(_aFunction(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,o){return e.call(t,n,r,o)}}return function(){return e.apply(t,arguments)}},$export=function(e,t,n){var r,o,i,a,s=e&$export.F,c=e&$export.G,l=e&$export.S,u=e&$export.P,d=e&$export.B,h=c?J:l?J[t]||(J[t]={}):(J[t]||{}).prototype,p=c?X:X[t]||(X[t]={}),f=p.prototype||(p.prototype={});for(r in c&&(n=t),n)i=((o=!s&&h&&void 0!==h[r])?h:n)[r],a=d&&o?_ctx(i,J):u&&"function"==typeof i?_ctx(Function.call,i):i,h&&ue(h,r,i,e&$export.U),p[r]!=i&&ie(p,r,a),u&&f[r]!=i&&(f[r]=i)};J.core=X,$export.F=1,$export.G=2,$export.S=4,$export.P=8,$export.B=16,$export.W=32,$export.U=64,$export.R=128;var de=$export,he=createCommonjsModule((function(e){var t=_uid("meta"),n=oe.f,r=0,o=Object.isExtensible||function(){return!0},i=!_fails((function(){return o(Object.preventExtensions({}))})),setMeta=function(e){n(e,t,{value:{i:"O"+ ++r,w:{}}})},a=e.exports={KEY:t,NEED:!1,fastKey:function(e,n){if(!Q(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!_has(e,t)){if(!o(e))return"F";if(!n)return"E";setMeta(e)}return e[t].i},getWeak:function(e,n){if(!_has(e,t)){if(!o(e))return!0;if(!n)return!1;setMeta(e)}return e[t].w},onFreeze:function(e){return i&&a.NEED&&o(e)&&!_has(e,t)&&setMeta(e),e}}})),pe=(he.KEY,he.NEED,he.fastKey,he.getWeak,he.onFreeze,createCommonjsModule((function(e){var t=ce("wks"),n=J.Symbol,r="function"==typeof n;(e.exports=function(e){return t[e]||(t[e]=r&&n[e]||(r?n:_uid)("Symbol."+e))}).store=t}))),fe=oe.f,me=pe("toStringTag"),_setToStringTag=function(e,t,n){e&&!_has(e=n?e:e.prototype,me)&&fe(e,me,{configurable:!0,value:t})},ge={f:pe},_e=oe.f,_wksDefine=function(e){var t=X.Symbol||(X.Symbol=J.Symbol||{});"_"==e.charAt(0)||e in t||_e(t,e,{value:ge.f(e)})},ye={}.toString,_cof=function(e){return ye.call(e).slice(8,-1)},ve=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==_cof(e)?e.split(""):Object(e)},_defined=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e},_toIobject=function(e){return ve(_defined(e))},be=Math.ceil,we=Math.floor,_toInteger=function(e){return isNaN(e=+e)?0:(e>0?we:be)(e)},Se=Math.min,_toLength=function(e){return e>0?Se(_toInteger(e),9007199254740991):0},xe=Math.max,Ee=Math.min,_toAbsoluteIndex=function(e,t){return(e=_toInteger(e))<0?xe(e+t,0):Ee(e,t)},_arrayIncludes=function(e){return function(t,n,r){var o,i=_toIobject(t),a=_toLength(i.length),s=_toAbsoluteIndex(r,a);if(e&&n!=n){for(;a>s;)if((o=i[s++])!=o)return!0}else for(;a>s;s++)if((e||s in i)&&i[s]===n)return e||s||0;return!e&&-1}},Oe=ce("keys"),_sharedKey=function(e){return Oe[e]||(Oe[e]=_uid(e))},ke=_arrayIncludes(!1),Ce=_sharedKey("IE_PROTO"),_objectKeysInternal=function(e,t){var n,r=_toIobject(e),o=0,i=[];for(n in r)n!=Ce&&_has(r,n)&&i.push(n);for(;t.length>o;)_has(r,n=t[o++])&&(~ke(i,n)||i.push(n));return i},je="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(","),Pe=Object.keys||function keys(e){return _objectKeysInternal(e,je)},Me={f:Object.getOwnPropertySymbols},Ae={f:{}.propertyIsEnumerable},Te=Array.isArray||function isArray(e){return"Array"==_cof(e)},_toObject=function(e){return Object(_defined(e))},Re=Z?Object.defineProperties:function defineProperties(e,t){_anObject(e);for(var n,r=Pe(t),o=r.length,i=0;o>i;)oe.f(e,n=r[i++],t[n]);return e},Ne=J.document,qe=Ne&&Ne.documentElement,Ie=_sharedKey("IE_PROTO"),Empty=function(){},createDict=function(){var e,t=_domCreate("iframe"),n=je.length;for(t.style.display="none",qe.appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("