├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yaml └── workflows │ └── ha-config-check.yml ├── .gitignore ├── .mdlrc ├── .yamllint ├── LICENSE.md ├── README.md ├── appdaemon ├── apps │ ├── Monitor-App │ │ ├── home_presence_app.py │ │ ├── home_presence_app.yaml │ │ └── requirements.txt │ ├── apps.yaml │ ├── check-config │ │ └── checkconfig.py │ ├── common │ │ ├── base.py │ │ └── globals.py │ └── util │ │ ├── presence_helper.py │ │ └── presence_helper.yaml └── dashboards │ └── Hello.dash ├── bin ├── README.md ├── _parse_yaml.sh ├── check_ci_status.sh ├── git_commit.sh ├── git_fetch.sh ├── make_fake_secrets.sh ├── test.sh └── update_radio_stations.sh ├── blueprints └── automation │ └── homeassistant │ ├── motion_light.yaml │ └── notify_leaving_zone.yaml ├── configuration.yaml ├── custom_components ├── .gitignore ├── __init__.py ├── arpscan_tracker │ ├── __init__.py │ ├── device_tracker.py │ └── manifest.json ├── attributes │ ├── __init__.py │ ├── manifest.json │ └── sensor.py ├── average │ ├── __init__.py │ ├── const.py │ ├── manifest.json │ └── sensor.py ├── beward │ ├── __init__.py │ ├── binary_sensor.py │ ├── camera.py │ ├── const.py │ ├── manifest.json │ └── sensor.py ├── car_wash │ ├── __init__.py │ ├── binary_sensor.py │ ├── const.py │ ├── manifest.json │ └── translations │ │ ├── binary_sensor.en.json │ │ ├── binary_sensor.it.json │ │ ├── binary_sensor.pl.json │ │ ├── binary_sensor.ru.json │ │ └── binary_sensor.uk.json ├── gismeteo │ ├── __init__.py │ ├── api.py │ ├── cache.py │ ├── config_flow.py │ ├── const.py │ ├── entity.py │ ├── manifest.json │ ├── sensor.py │ ├── translations │ │ ├── en.json │ │ ├── pl.json │ │ ├── ru.json │ │ ├── sensor.en.json │ │ ├── sensor.pl.json │ │ ├── sensor.ru.json │ │ └── sensor.uk.json │ └── weather.py ├── hacs │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── acknowledge_critical_repository.py │ │ ├── check_local_path.py │ │ ├── get_critical_repositories.py │ │ ├── hacs_config.py │ │ ├── hacs_removed.py │ │ ├── hacs_repositories.py │ │ ├── hacs_repository.py │ │ ├── hacs_repository_data.py │ │ ├── hacs_settings.py │ │ └── hacs_status.py │ ├── base.py │ ├── config_flow.py │ ├── const.py │ ├── enums.py │ ├── hacsbase │ │ ├── __init__.py │ │ ├── configuration.py │ │ ├── data.py │ │ └── hacs.py │ ├── helpers │ │ ├── __init__.py │ │ ├── classes │ │ │ ├── __init__.py │ │ │ ├── exceptions.py │ │ │ ├── manifest.py │ │ │ ├── removed.py │ │ │ ├── repository.py │ │ │ ├── repositorydata.py │ │ │ └── validate.py │ │ ├── functions │ │ │ ├── __init__.py │ │ │ ├── configuration_schema.py │ │ │ ├── constrains.py │ │ │ ├── download.py │ │ │ ├── filters.py │ │ │ ├── get_list_from_default.py │ │ │ ├── information.py │ │ │ ├── is_safe_to_remove.py │ │ │ ├── logger.py │ │ │ ├── misc.py │ │ │ ├── path_exsist.py │ │ │ ├── register_repository.py │ │ │ ├── remaining_github_calls.py │ │ │ ├── save.py │ │ │ ├── store.py │ │ │ ├── template.py │ │ │ ├── validate_repository.py │ │ │ └── version_to_install.py │ │ ├── methods │ │ │ ├── __init__.py │ │ │ ├── installation.py │ │ │ ├── registration.py │ │ │ └── reinstall_if_needed.py │ │ └── properties │ │ │ ├── __init__.py │ │ │ ├── can_be_installed.py │ │ │ ├── custom.py │ │ │ └── pending_update.py │ ├── iconset.js │ ├── manifest.json │ ├── models │ │ ├── __init__.py │ │ ├── core.py │ │ ├── frontend.py │ │ └── system.py │ ├── operational │ │ ├── __init__.py │ │ ├── backup.py │ │ ├── factory.py │ │ ├── reload.py │ │ ├── remove.py │ │ ├── runtime.py │ │ ├── setup.py │ │ └── setup_actions │ │ │ ├── __init__.py │ │ │ ├── categories.py │ │ │ ├── clear_storage.py │ │ │ ├── frontend.py │ │ │ ├── load_hacs_repository.py │ │ │ ├── sensor.py │ │ │ └── websocket_api.py │ ├── repositories │ │ ├── __init__.py │ │ ├── appdaemon.py │ │ ├── integration.py │ │ ├── netdaemon.py │ │ ├── plugin.py │ │ ├── python_script.py │ │ └── theme.py │ ├── sensor.py │ ├── share.py │ ├── system_health.py │ ├── translations │ │ └── en.json │ ├── validate │ │ ├── README.md │ │ ├── __init__.py │ │ ├── base.py │ │ ├── common │ │ │ ├── hacs_manifest.py │ │ │ ├── repository_description.py │ │ │ ├── repository_information_file.py │ │ │ └── repository_topics.py │ │ └── integration │ │ │ └── integration_manifest.py │ └── webresponses │ │ ├── __init__.py │ │ └── frontend.py ├── iaquk │ ├── __init__.py │ ├── const.py │ ├── manifest.json │ ├── sensor.py │ └── translations │ │ ├── sensor.en.json │ │ └── sensor.ru.json ├── jq300 │ ├── __init__.py │ ├── api.py │ ├── binary_sensor.py │ ├── config_flow.py │ ├── const.py │ ├── entity.py │ ├── manifest.json │ ├── sensor.py │ ├── translations │ │ ├── en.json │ │ └── ru.json │ └── util.py ├── linkplay │ ├── __init__.py │ ├── config_flow.py │ ├── manifest.json │ ├── media_player.py │ └── services.yaml ├── narodmon │ ├── __init__.py │ ├── api.py │ ├── config_flow.py │ ├── const.py │ ├── manifest.json │ ├── sensor.py │ └── translations │ │ ├── en.json │ │ └── ru.json ├── pfsense_gateways │ ├── __init__.py │ ├── manifest.json │ └── sensor.py ├── sensor │ ├── attributes.py │ └── sensor.attributes.markdown ├── shelly │ ├── .translations │ │ └── en.json │ ├── __init__.py │ ├── binary_sensor.py │ ├── block.py │ ├── config_flow.py │ ├── configuration_schema.py │ ├── const.py │ ├── cover.py │ ├── device.py │ ├── light.py │ ├── manifest.json │ ├── sensor.py │ ├── services.yaml │ ├── switch.py │ └── translations │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ └── it.json └── snowtire │ ├── __init__.py │ ├── binary_sensor.py │ ├── const.py │ ├── manifest.json │ └── translations │ ├── binary_sensor.en.json │ ├── binary_sensor.hu.json │ ├── binary_sensor.it.json │ ├── binary_sensor.pl.json │ ├── binary_sensor.ru.json │ └── binary_sensor.uk.json ├── docs └── images │ ├── desktop_automations.jpg │ ├── desktop_home.jpg │ ├── desktop_home_info.jpg │ ├── desktop_system_info.jpg │ ├── mobile_automations.jpg │ ├── mobile_home.jpg │ ├── mobile_home_info.jpg │ └── mobile_system_info.jpg ├── esphome ├── .gitignore ├── README.md ├── bathroom.yaml ├── kitchen.yaml └── kitchen_old.yaml ├── external_addons ├── README.md └── ha_host_monitor.py ├── gpg_keys └── limych_public_key.asc ├── logging ├── history.yaml ├── logbook.yaml └── logger.yaml ├── lovelace ├── 00_home_view.yaml ├── 10_weather_view.yaml ├── 20_home_info_view.yaml ├── 30_lights_view.yaml ├── 40_system_info_view.yaml ├── 50_security_view.yaml ├── 90_settings_view.yaml ├── cards │ ├── dev.yaml │ ├── dev_content.yaml │ ├── divider_card.yaml │ ├── empty.yaml │ ├── empty_card.yaml │ ├── laundry.yaml │ ├── speakers.yaml │ ├── weather_windy.yaml │ └── zigbee.yaml └── system │ ├── hass_monitor_card.yaml │ └── nas_monitor_card.yaml ├── packages ├── areas │ ├── bathroom.yaml │ ├── bedroom.yaml │ ├── hallway.yaml │ ├── kitchen.yaml │ ├── laundry.yaml │ └── living_room.yaml ├── battery_alert.yaml ├── car.yaml ├── day_mode.yaml ├── device_tracker.yaml ├── house │ ├── cleaning_day.yaml │ ├── guest_mode.yaml │ └── shelly.yaml ├── interactive │ ├── README.md │ ├── notifications.yaml │ └── telegram_actions.yaml ├── media.yaml ├── power_monitoring.yaml ├── presence_detection.yaml ├── security │ ├── alerts.yaml │ ├── cameras.yaml │ └── ip_ban.yaml ├── system │ ├── clean_temp.yaml │ ├── devices_monitor.yaml │ ├── git_backup.yaml │ ├── hassio_monitor.yaml │ ├── internet_monitor.yaml │ ├── nas_monitor.yaml │ ├── system_info.yaml │ └── zigbee2mqtt.yaml ├── temp_light_humidity.yaml ├── tts.yaml ├── vacation_mode.yaml ├── wallpanel.yaml └── weather.yaml ├── radio_stations.yaml ├── scenes └── .gitkeep ├── tests ├── example.com.fake_crt ├── example.com.fake_key ├── fake_secrets.yaml └── markdown.style.rb ├── themes ├── green_dark_mode │ └── green_dark_mode.yaml ├── midnight │ └── midnight.yaml └── orange_dark │ └── orange_dark.yaml └── ui-lovelace.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 88 10 | tab_width = 4 11 | 12 | [*.{yml,yaml}] 13 | indent_size = 2 14 | 15 | [*.json] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/.gitattributes -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: limych 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: 'https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=UAGFL5L6M8RN2&item_name=[hass-config]+Donation+for+a+big+barrel+of+coffee+:)¤cy_code=EUR&source=url' 13 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/ha-config-check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Check configuration" 3 | 4 | on: 5 | push: 6 | pull_request: 7 | schedule: 8 | - cron: 0 2 * * * 9 | 10 | jobs: 11 | # YAML Lint 12 | yamllint: 13 | name: YAML Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out configuration from GitHub 17 | uses: actions/checkout@v2.4.0 18 | - name: Run yamllint 19 | uses: frenck/action-yamllint@v1.1 20 | 21 | #Remark Lint 22 | remarklint: 23 | name: Remark Lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Check out configuration from GitHub 27 | uses: actions/checkout@v2.4.0 28 | - name: Running Remark lint 29 | uses: "docker://pipelinecomponents/remark-lint:latest" 30 | continue-on-error: true 31 | with: 32 | args: "remark --no-stdout --color --frail --use preset-lint-recommended ." 33 | 34 | #Home Assistant - Config check 35 | home_assistant: 36 | name: "Home Assistant Core ${{ matrix.version }} Configuration Check" 37 | runs-on: ubuntu-latest 38 | needs: [yamllint, remarklint] 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | version: ["stable", "beta", "dev"] 43 | steps: 44 | - name: Check out configuration from GitHub 45 | uses: actions/checkout@v2.4.0 46 | - name: Create dummy files 47 | run: | 48 | touch ./home-assistant.log 49 | # - perl -i -pe 'if (/whitelist_external_dirs:/) { $_ = <>; $_ = <> while /^\s*-/; }' configuration.yaml 50 | # - sed -i 's|use_x_forwarded_for:.*||g; s|trusted_proxies:.*||g' configuration.yaml 51 | # # - perl -i -pe 'while (/- platform: rpi_gpio/) { $_ = <>; $_ = <> while !/^\s*-/ && !eof(); }' packages/areas/hallway.yaml 52 | # - rm packages/areas/hallway.yaml 53 | - name: Run Home Assistant Configuration Check 54 | uses: frenck/action-home-assistant@v1.3 55 | with: 56 | secrets: ./tests/fake_secrets.yaml 57 | version: "${{ matrix.version }}" 58 | 59 | esphome: 60 | name: "ESPHome check" 61 | runs-on: ubuntu-latest 62 | needs: [yamllint] 63 | steps: 64 | - name: Check out configuration from GitHub 65 | uses: actions/checkout@v2.4.0 66 | - name: Set up Python 3.8 67 | uses: actions/setup-python@v2.3.1 68 | with: 69 | python-version: 3.8 70 | - name: Install dependencies 71 | run: | 72 | python -m pip install --upgrade pip setuptools wheel 73 | pip install esphome 74 | pip list 75 | esphome version 76 | - name: Copy stub files into configuration folder 77 | run: cp -R ./tests/fake_secrets.yaml ./esphome/secrets.yaml 78 | - name: Run esphome on all files 79 | # yamllint disable rule:line-length 80 | run: | 81 | for file in $(find ./esphome -type f -name "*.yaml" -not -name "secrets.yaml"); do esphome "${file}" config; done 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | !/**/.gitkeep 3 | 4 | core* 5 | home-assistant* 6 | 7 | /www/ 8 | !/www/custom-lovelace/ 9 | !/www/community/ 10 | 11 | /lovelace/99_test_view.yaml 12 | /lovelace/cards/links.yaml 13 | /packages/test_package.yaml 14 | /packages/persons/ 15 | 16 | /appdaemon/appdaemon.yaml 17 | /appdaemon/compiled 18 | /appdaemon/apps/security/ 19 | 20 | 21 | # Python byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | 26 | 27 | # Ignore Extensions 28 | *.db 29 | *.old 30 | *.log 31 | *.conf 32 | *.HA_VERSION 33 | *.db-shm 34 | *.db-wal 35 | *.crt 36 | *.csr 37 | *.key 38 | *.pem 39 | *.pid 40 | *.xml 41 | 42 | 43 | # Ignore Folders 44 | /.*/ 45 | !/.github/ 46 | # 47 | /.ssh/ 48 | /deps/ 49 | /gh-pages/ 50 | /img_sources/ 51 | /tts/ 52 | /zones/ 53 | 54 | 55 | # Ensure these YAML files are ignored, otherwise your secret data/credentials will leak. 56 | ip_bans.yaml 57 | secrets.yaml 58 | security.yaml 59 | known_devices.yaml 60 | alarm_control_panel.yaml 61 | emulated_hue_ids.json 62 | security_automations.yaml 63 | emergency_call.yaml 64 | emergency_telegram.yaml 65 | google_calendars.yaml 66 | 67 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD026", "~MD013" 2 | style "/config/tests/markdown.style.rb" 3 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | ignore: | 3 | custom_components/ 4 | ip_bans.yaml 5 | themes/ 6 | .github/ 7 | rules: 8 | braces: 9 | level: error 10 | min-spaces-inside: 0 11 | max-spaces-inside: 1 12 | min-spaces-inside-empty: -1 13 | max-spaces-inside-empty: -1 14 | brackets: 15 | level: error 16 | min-spaces-inside: 0 17 | max-spaces-inside: 0 18 | min-spaces-inside-empty: -1 19 | max-spaces-inside-empty: -1 20 | colons: 21 | level: error 22 | max-spaces-before: 0 23 | max-spaces-after: 1 24 | commas: 25 | level: error 26 | max-spaces-before: 0 27 | min-spaces-after: 1 28 | max-spaces-after: 1 29 | comments: 30 | level: error 31 | require-starting-space: true 32 | min-spaces-from-content: 2 33 | comments-indentation: 34 | level: error 35 | document-end: 36 | level: error 37 | present: false 38 | document-start: 39 | level: error 40 | present: true 41 | ignore: | 42 | config/known_devices.yaml 43 | empty-lines: 44 | level: error 45 | max: 2 46 | max-start: 0 47 | max-end: 1 48 | hyphens: 49 | level: error 50 | max-spaces-after: 1 51 | indentation: 52 | level: error 53 | spaces: 2 54 | indent-sequences: true 55 | check-multi-line-strings: false 56 | key-duplicates: 57 | level: error 58 | line-length: 59 | ignore: | 60 | .github/support.yml 61 | .github/settings.yml 62 | level: warning 63 | max: 120 64 | allow-non-breakable-words: true 65 | allow-non-breakable-inline-mappings: true 66 | new-line-at-end-of-file: 67 | level: error 68 | new-lines: 69 | level: error 70 | type: unix 71 | trailing-spaces: 72 | level: error 73 | truthy: 74 | level: error 75 | -------------------------------------------------------------------------------- /appdaemon/apps/Monitor-App/home_presence_app.yaml: -------------------------------------------------------------------------------- 1 | home_presence_app: 2 | module: home_presence_app 3 | class: HomePresenceApp 4 | plugin: 5 | - HASS 6 | - MQTT 7 | monitor_topic: presence 8 | #mqtt_event: MQTT 9 | user_device_domain: device_tracker 10 | #everyone_not_home: everyone_not_home 11 | #everyone_home: everyone_home 12 | #somebody_is_home: somebody_is_home 13 | depart_check_time: 30 14 | depart_scans: 3 15 | minimum_confidence: 60 16 | not_home_timeout: 15 17 | system_check: 30 18 | system_timeout: 60 19 | home_gateway_sensors: 20 | - binary_sensor.front_door 21 | 22 | # reboot the all nodes at 12 midnight on Mondays and Thursdays 23 | # scheduled_restart: 24 | # time: 00:00:01 25 | # days: 26 | # - mon 27 | # - thu 28 | # location: all 29 | 30 | # other location configuration options 31 | # location: living_room, kitchen 32 | 33 | #location: 34 | # - living_room 35 | # - kitchen 36 | 37 | home_motion_sensors: 38 | - binary_sensor.hallway_motion 39 | - binary_sensor.kitchen_motion 40 | - binary_sensor.bedroom_motion 41 | - binary_sensor.living_room_motion 42 | 43 | log_level: DEBUG 44 | known_devices: 45 | - "DE:CB:5B:68:7C:F1 Limych Phone B" 46 | # - "D0:F8:8C:A1:EF:CB Limych Tablet L" 47 | 48 | known_beacons: 49 | - "80:EA:CA:83:51:10 Limych_Watch" 50 | 51 | remote_monitors: 52 | disable: False 53 | 54 | hallway: 55 | auto_reboot_when_offline: False 56 | 57 | bedroom: 58 | host: !secret bedroom_monitor_host 59 | username: !secret bedroom_monitor_username 60 | password: !secret bedroom_monitor_password 61 | reboot_command: sudo /sbin/reboot now 62 | auto_reboot_when_offline: True 63 | time: 02:00:01 64 | 65 | -------------------------------------------------------------------------------- /appdaemon/apps/Monitor-App/requirements.txt: -------------------------------------------------------------------------------- 1 | paramiko 2 | -------------------------------------------------------------------------------- /appdaemon/apps/apps.yaml: -------------------------------------------------------------------------------- 1 | global_modules: globals 2 | -------------------------------------------------------------------------------- /appdaemon/apps/common/globals.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | # Global variables 4 | 5 | notify_limych = "notify/limych" 6 | 7 | limych = "person.limych" 8 | 9 | alarm = "alarm_control_panel.house" 10 | 11 | 12 | notification_mode = {} 13 | notification_mode["start_quiet_weekday"] = "23:00:00" 14 | notification_mode["start_quiet_weekend"] = "23:59:00" 15 | notification_mode["stop_quiet_weekday"] = "07:00:00" 16 | notification_mode["stop_quiet_weekend"] = "09:00:00" 17 | 18 | 19 | presence_state = {} 20 | # Change this if you want to change the display name 21 | presence_state["home"] = "Home" 22 | presence_state["just_arrived"] = "Just arrived" 23 | presence_state["just_left"] = "Just left" 24 | presence_state["away"] = "Away" 25 | presence_state["extended_away"] = "Extended away" 26 | 27 | 28 | PEOPLE = { 29 | 'Limych': { 30 | 'device_tracker': 'device_tracker.limych_addon_presence', 31 | 'proximity': 'proximity.home_limych', 32 | 'notifier': 'limych' 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /appdaemon/apps/util/presence_helper.py: -------------------------------------------------------------------------------- 1 | from base import Base 2 | import globals 3 | from globals import PEOPLE 4 | 5 | class PresenceHelper(Base): 6 | 7 | def initialize(self) -> None: 8 | """Initialize.""" 9 | super().initialize() 10 | 11 | def anyone_home(self, **kwargs:dict) -> bool: 12 | limych = self.get_state(PEOPLE['Limych']['device_tracker']) 13 | if (limych == "Just arrived" or limych == "Home"): 14 | return True 15 | else: 16 | return False 17 | 18 | def anyone_just_arrived(self, **kwargs:dict) -> bool: 19 | limych = self.get_state(PEOPLE['Limych']['device_tracker']) 20 | if (limych == "Just arrived"): 21 | return True 22 | else: 23 | return False 24 | 25 | def limych_home_alone(self, **kwargs:dict) -> bool: 26 | limych = self.get_state(PEOPLE['Limych']['device_tracker']) 27 | if (limych == "Just arrived" or limych == "Home"): 28 | return True 29 | else: 30 | return False -------------------------------------------------------------------------------- /appdaemon/apps/util/presence_helper.yaml: -------------------------------------------------------------------------------- 1 | presence_helper: 2 | module: presence_helper 3 | class: PresenceHelper -------------------------------------------------------------------------------- /appdaemon/dashboards/Hello.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Hello Panel 5 | widget_dimensions: [120, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | 9 | label: 10 | widget_type: label 11 | text: Hello World 12 | 13 | layout: 14 | - label(2x2) 15 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | # Shell scripts 2 | 3 | These scripts are run on the host system for maintenence purposes. They can be run via scripts in homeassistant if required. 4 | 5 | All scripts are written on strict Bourne shell for compatibility reason. 6 | 7 | The Github bash scripts here were inspired by BrianJKing - https://github.com/brianjking/homeassistant-config , and Tommatheussen - https://github.com/Tommatheussen/Home-Assistant-Configuration 8 | 9 | ## _parse_yaml.sh 10 | 11 | Service function for parsing YAML files. 12 | 13 | ## cert_update.sh 14 | 15 | Script which update SSL-certificates from remote host. 16 | 17 | ## make_fake_secrets.sh 18 | 19 | Script which for testing purposes generate fake secrets.yaml-file based on real file. 20 | -------------------------------------------------------------------------------- /bin/_parse_yaml.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ## 3 | ## This is service function which can parse YAML-file. ## 4 | ## 5 | 6 | # Source: https://gist.github.com/pkuczynski/8665367 7 | 8 | if test -z "$(which gawk)"; then 9 | echo "ERROR! GNU awk not found." 10 | exit 1 11 | fi 12 | 13 | parse_yaml() { 14 | local prefix=${2:-'secret_'} 15 | local callback=${3:-'%s%s%s=\"%s\";'} 16 | local s='[[:space:]]*' w='[a-zA-Z0-9_\-]*' fs=$(echo @|tr @ '\034'|tr -d '\015') 17 | sed -ne "s|,$s\]$s\$|]|" \ 18 | -e ":1;s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1\2: [\3]\n\1 - \4|;t1" \ 19 | -e "s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s\]|\1\2:\n\1 - \3|;p" $1 | \ 20 | sed -ne "s|,$s}$s\$|}|" \ 21 | -e ":1;s|^\($s\)-$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1- {\2}\n\1 \3: \4|;t1" \ 22 | -e "s|^\($s\)-$s{$s\(.*\)$s}|\1-\n\1 \2|;p" | \ 23 | sed -ne "s|^\($s\):|\1|" \ 24 | -e "s|^\($s\)-$s[\"']\(.*\)[\"']$s\$|\1$fs$fs\2|p" \ 25 | -e "s|^\($s\)-$s\(.*\)$s\$|\1$fs$fs\2|p" \ 26 | -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \ 27 | -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" | \ 28 | sed 's/"/\\"/g' | \ 29 | gawk -F$fs "{ 30 | indent = length(\$1)/2; 31 | vname[indent] = \$2; 32 | for (i in vname) {if (i > indent) {delete vname[i]}} 33 | if (length(\$3) > 0) { 34 | vn=\"\"; for (i=0; i 6 | # Creative Commons BY-NC-SA 4.0 International Public License 7 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | # 9 | 10 | WDIR=$(cd `dirname $0` && pwd) 11 | ROOT=$(dirname ${WDIR}) 12 | DATA_DIR='/tmp' 13 | 14 | # Check for required utilites and install it if they are not available 15 | test -n "$(which gawk)" || apk -q add gawk 16 | 17 | # Include parse_yaml function 18 | . ${WDIR}/_parse_yaml.sh 19 | 20 | # Read yaml file 21 | eval $(parse_yaml ${ROOT}/secrets.yaml) 22 | 23 | REPO_ID_FPATH="${DATA_DIR}/travisci_repo_id.txt" 24 | LAST_BUILD_FPATH="${DATA_DIR}/travisci_last_build.txt" 25 | 26 | if [ ! -f "${REPO_ID_FPATH}" ]; then 27 | curl -s https://api.travis-ci.org/repos/${secret_github_repo} | jq .id >"${REPO_ID_FPATH}" 28 | fi 29 | if [ ! -f "${LAST_BUILD_FPATH}" ]; then 30 | touch "${LAST_BUILD_FPATH}" 31 | fi 32 | 33 | repo_id=`cat ${REPO_ID_FPATH}` 34 | last_build=`cat ${LAST_BUILD_FPATH}` 35 | 36 | report=`curl -s -H "Travis-API-Version: 3" "https://api.travis-ci.org/repo/${repo_id}/branch/master"` 37 | 38 | current_build=`echo "${report}" | jq .last_build.number` 39 | current_build="${current_build%\"}" 40 | current_build="${current_build#\"}" 41 | 42 | current_state=`echo "${report}" | jq .last_build.state` 43 | current_state="${current_state%\"}" 44 | current_state="${current_state#\"}" 45 | 46 | if [ "$current_state" == "passed" -a "$current_build" != "$last_build" ]; then 47 | echo "$current_build" >"${LAST_BUILD_FPATH}" 48 | exit 49 | fi 50 | 51 | exit 1 52 | -------------------------------------------------------------------------------- /bin/git_commit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script pushes my selected files to my GitHub repository 4 | # 5 | # Copyright (c) 2020-2021, Andrey "Limych" Khrolenok 6 | # Creative Commons BY-NC-SA 4.0 International Public License 7 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | # 9 | 10 | WDIR=$(cd `dirname $0` && pwd) 11 | ROOT=$(dirname ${WDIR}) 12 | 13 | # Check for required utilites and install it if they are not available 14 | git --version >/dev/null || apk -q add git 15 | test -n "$(which gawk)" || apk -q add gawk 16 | 17 | 18 | # Include parse_yaml function 19 | . ${WDIR}/_parse_yaml.sh || exit 1 20 | 21 | # Read yaml file 22 | eval $(parse_yaml ${ROOT}/secrets.yaml) 23 | 24 | cd ${ROOT} 25 | 26 | git config user.name "${secret_git_user_name}" 27 | git config user.email "${secret_git_user_email}" 28 | git config core.sshCommand "ssh -i ${ROOT}/.ssh/id_rsa -oStrictHostKeyChecking=no" 29 | 30 | if [ "${ROOT}/secrets.yaml" -nt "${ROOT}/tests/fake_secrets.yaml" ]; then 31 | echo "Updating fake_secrets.yaml" 32 | ${ROOT}/bin/make_fake_secrets.sh 33 | fi 34 | 35 | git add . &&\ 36 | git commit -m "${1:-Config files on `date +'%d-%m-%Y %H:%M:%S'`}" &&\ 37 | git push -u origin master 38 | 39 | -------------------------------------------------------------------------------- /bin/git_fetch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script pulls my selected files from my GitHub repository and 4 | # treats them as the 'master' copy 5 | # 6 | # Copyright (c) 2020-2021, Andrey "Limych" Khrolenok 7 | # Creative Commons BY-NC-SA 4.0 International Public License 8 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 9 | # 10 | 11 | WDIR=$(cd `dirname $0` && pwd) 12 | ROOT=$(dirname ${WDIR}) 13 | 14 | # Check for required utilites and install it if they are not available 15 | git --version >/dev/null || apk -q add git 16 | gpg --version >/dev/null || apk -q add gnupg 17 | test -n "$(which gawk)" || apk -q add gawk 18 | 19 | # Include parse_yaml function 20 | . ${WDIR}/_parse_yaml.sh 21 | 22 | # Read yaml file 23 | eval $(parse_yaml ${ROOT}/secrets.yaml) 24 | 25 | cd ${ROOT} 26 | git config user.name "${secret_git_user_name}" 27 | git config user.email "${secret_git_user_email}" 28 | git config core.sshCommand "ssh -i ${ROOT}/.ssh/id_rsa -oStrictHostKeyChecking=no" 29 | git fetch origin master 30 | 31 | gpg --import ${ROOT}/gpg_keys/*.asc 32 | if git verify-commit origin/master; then 33 | # Update files only if commit is verified. Then restart Home Assistant 34 | git reset --hard origin/master && hassio homeassistant restart 35 | exit 36 | fi 37 | 38 | echo "Commit verification failed" 39 | exit 1 40 | -------------------------------------------------------------------------------- /bin/make_fake_secrets.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script parse real secrets.yaml-file and based on it 4 | # generate fake one for testing reasons. 5 | # 6 | # Copyright (c) 2020-2021, Andrey "Limych" Khrolenok 7 | # Creative Commons BY-NC-SA 4.0 International Public License 8 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 9 | # 10 | 11 | WDIR=$(cd `dirname $0` && pwd) 12 | ROOT=$(dirname ${WDIR}) 13 | SEED=$$ 14 | 15 | # Check for required utilites and install it if they are not available 16 | test -n "$(which gawk)" || apk -q add gawk 17 | 18 | # Define faker callback 19 | faker() { 20 | local key=$3 value=$4 21 | 22 | if [ "$key" == "ssl_certificate" ]; then 23 | value='tests/example.com.fake_crt' 24 | elif [ "$key" == "ssl_key" ]; then 25 | value='tests/example.com.fake_key' 26 | elif [ "$key" == "home_timezone" ]; then 27 | value='America/Los_Angeles' 28 | elif echo ${key} | grep -q '_\(login\|username\|password\)$'; then 29 | value='super_5EcREt' 30 | elif echo ${key} | grep -q '_\(lat\|lon\|latitude\|longitude\)$'; then 31 | value='00.000000' 32 | else 33 | SEED=$(expr ${SEED} + 1) 34 | value=$(echo ${value} | gawk 'BEGIN {srand('${SEED}'); OFS = ""} { n = split($0, a, ""); for (i = 1; i <= n; i++) { if (a[i] ~ /[[:digit:]]/) { new = new int(rand() * 10) } else if (a[i] ~ /[[:alpha:]]/) { new = new sprintf("%c", int(rand() * 26 + 97)) } else { new = new a[i] } }; $0 = new; print }') 35 | fi 36 | 37 | echo "$key: \"$value\"" 38 | } 39 | 40 | 41 | 42 | # Include parse_yaml function 43 | . ${WDIR}/_parse_yaml.sh 44 | 45 | # Read real yaml file and make fake one 46 | FPATH=${ROOT}/tests/fake_secrets.yaml 47 | >${FPATH} 48 | echo "#" >>${FPATH} 49 | echo "# ATTENTION! This is autogenerated fake file. All values filled random characters." >>${FPATH} 50 | echo "# Don't edit this file -- all changes will be lost on next file auto generation." >>${FPATH} 51 | echo "#" >>${FPATH} 52 | eval $(parse_yaml ${ROOT}/secrets.yaml '' 'faker \"%s\" \"%s\" \"%s\" \"%s\";') >>${FPATH} 53 | 54 | exit 55 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #set -x 4 | 5 | WDIR=$(cd `dirname $0` && pwd) 6 | ROOT=$(dirname ${WDIR}) 7 | 8 | # Check for required utilites and install it if they are not available 9 | test -n "$(which gawk)" || apk -q add gawk 10 | 11 | # Include parse_yaml function 12 | . ${WDIR}/_parse_yaml.sh 13 | 14 | # Read yaml file 15 | echo $(parse_yaml ${ROOT}/secrets.yaml) 16 | 17 | -------------------------------------------------------------------------------- /bin/update_radio_stations.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script updates radio stations list in configs 4 | # 5 | # Copyright (c) 2020, Andrey "Limych" Khrolenok 6 | # Creative Commons BY-NC-SA 4.0 International Public License 7 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | # 9 | 10 | WDIR=$(cd `dirname $0` && pwd) 11 | ROOT=$(dirname ${WDIR}) 12 | 13 | STATIONS_FPATH=${ROOT}/radio_stations.yaml # From here we get list of radio stations 14 | MEDIA_FPATH=${ROOT}/packages/media.yaml # There we patch configs 15 | 16 | INPUT="" 17 | AUTOMATION="" 18 | 19 | s='[[:space:]]*' 20 | 21 | # Check for required utilites and install it if they are not available 22 | test -n "$(which gawk)" || apk -q add gawk 23 | 24 | parse() { 25 | local callback=${2:-'%s%s%s=\"%s\"'} 26 | local w='[^:]*' fs=$(echo @|tr @ '\034'|tr -d '\015') 27 | sed -e "/^[ \t]*#/d" \ 28 | -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ 29 | -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" | 30 | gawk -F$fs "{ 31 | indent = length(\$1)/2; 32 | vname[indent] = \$2; 33 | for (i in vname) {if (i > indent) {delete vname[i]}} 34 | if (length(\$3) > 0) { 35 | vn=\"\"; for (i=0; i"$TMP_FPATH" 62 | mv -f "$TMP_FPATH" "$MEDIA_FPATH" 63 | 64 | exit 65 | -------------------------------------------------------------------------------- /blueprints/automation/homeassistant/motion_light.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Motion-activated Light 3 | description: Turn on a light when motion is detected. 4 | domain: automation 5 | source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml 6 | input: 7 | motion_entity: 8 | name: Motion Sensor 9 | selector: 10 | entity: 11 | domain: binary_sensor 12 | device_class: motion 13 | light_target: 14 | name: Light 15 | selector: 16 | target: 17 | entity: 18 | domain: light 19 | no_motion_wait: 20 | name: Wait time 21 | description: Time to leave the light on after last motion is detected. 22 | default: 120 23 | selector: 24 | number: 25 | min: 0 26 | max: 3600 27 | unit_of_measurement: seconds 28 | 29 | # If motion is detected within the delay, 30 | # we restart the script. 31 | mode: restart 32 | max_exceeded: silent 33 | 34 | trigger: 35 | platform: state 36 | entity_id: !input motion_entity 37 | from: "off" 38 | to: "on" 39 | 40 | action: 41 | - service: light.turn_on 42 | target: !input light_target 43 | - wait_for_trigger: 44 | platform: state 45 | entity_id: !input motion_entity 46 | from: "on" 47 | to: "off" 48 | - delay: !input no_motion_wait 49 | - service: light.turn_off 50 | target: !input light_target 51 | -------------------------------------------------------------------------------- /blueprints/automation/homeassistant/notify_leaving_zone.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Zone Notification 3 | description: Send a notification to a device when a person leaves a specific zone. 4 | domain: automation 5 | source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml 6 | input: 7 | person_entity: 8 | name: Person 9 | selector: 10 | entity: 11 | domain: person 12 | zone_entity: 13 | name: Zone 14 | selector: 15 | entity: 16 | domain: zone 17 | notify_device: 18 | name: Device to notify 19 | description: Device needs to run the official Home Assistant app to receive notifications. 20 | selector: 21 | device: 22 | integration: mobile_app 23 | 24 | trigger: 25 | platform: state 26 | entity_id: !input person_entity 27 | 28 | variables: 29 | zone_entity: !input zone_entity 30 | # This is the state of the person when it's in this zone. 31 | zone_state: "{{ states[zone_entity].name }}" 32 | person_entity: !input person_entity 33 | person_name: "{{ states[person_entity].name }}" 34 | 35 | condition: 36 | condition: template 37 | value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" 38 | 39 | action: 40 | domain: mobile_app 41 | type: notify 42 | device_id: !input notify_device 43 | message: "{{ person_name }} has left {{ zone_state }}" 44 | -------------------------------------------------------------------------------- /custom_components/.gitignore: -------------------------------------------------------------------------------- 1 | !*.py 2 | !*/ -------------------------------------------------------------------------------- /custom_components/__init__.py: -------------------------------------------------------------------------------- 1 | """Stub.""" 2 | -------------------------------------------------------------------------------- /custom_components/arpscan_tracker/__init__.py: -------------------------------------------------------------------------------- 1 | """The arpscan_tracker component.""" 2 | -------------------------------------------------------------------------------- /custom_components/arpscan_tracker/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "arpscan_tracker", 3 | "name": "Arpscan tracker", 4 | "version": "1.0.6", 5 | "documentation": "https://github.com/cyberjunky/home-assistant-arpscan_tracker", 6 | "issue_tracker": "https://github.com/cyberjunky/home-assistant-arpscan_tracker/issues", 7 | "requirements": [], 8 | "dependencies": [], 9 | "codeowners": [ 10 | "@cyberjunky" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/attributes/__init__.py: -------------------------------------------------------------------------------- 1 | """Attributes breakout sensor""" 2 | -------------------------------------------------------------------------------- /custom_components/attributes/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "attributes", 3 | "name": "Attributes", 4 | "version": "1.2.0", 5 | "documentation": "https://github.com/pilotak/homeassistant-attributes", 6 | "issue_tracker": "https://github.com/pilotak/homeassistant-attributes/issues", 7 | "requirements": [], 8 | "dependencies": [], 9 | "codeowners": ["@pilotak"] 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/average/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2019, Andrey "Limych" Khrolenok 3 | # Creative Commons BY-NC-SA 4.0 International Public License 4 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 5 | # 6 | """ 7 | The Average Temperature Sensor. 8 | 9 | For more details about this sensor, please refer to the documentation at 10 | https://github.com/Limych/ha-average/ 11 | """ 12 | -------------------------------------------------------------------------------- /custom_components/average/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Average Sensor. 3 | 4 | For more details about this sensor, please refer to the documentation at 5 | https://github.com/Limych/ha-average/ 6 | """ 7 | from datetime import timedelta 8 | 9 | # Base component constants 10 | NAME = "Average Sensor" 11 | DOMAIN = "average" 12 | VERSION = "1.7.3" 13 | ISSUE_URL = "https://github.com/Limych/ha-average/issues" 14 | 15 | STARTUP_MESSAGE = f""" 16 | ------------------------------------------------------------------- 17 | {NAME} 18 | Version: {VERSION} 19 | This is a custom integration! 20 | If you have ANY issues with this you need to open an issue here: 21 | {ISSUE_URL} 22 | ------------------------------------------------------------------- 23 | """ 24 | 25 | # Configuration and options 26 | CONF_START = "start" 27 | CONF_END = "end" 28 | CONF_DURATION = "duration" 29 | CONF_PRECISION = "precision" 30 | CONF_PERIOD_KEYS = [CONF_START, CONF_END, CONF_DURATION] 31 | CONF_PROCESS_UNDEF_AS = "process_undef_as" 32 | 33 | # Defaults 34 | DEFAULT_NAME = "Average" 35 | DEFAULT_PRECISION = 2 36 | 37 | # Attributes 38 | ATTR_START = "start" 39 | ATTR_END = "end" 40 | ATTR_SOURCES = "sources" 41 | ATTR_COUNT_SOURCES = "count_sources" 42 | ATTR_AVAILABLE_SOURCES = "available_sources" 43 | ATTR_COUNT = "count" 44 | ATTR_MIN_VALUE = "min_value" 45 | ATTR_MAX_VALUE = "max_value" 46 | # 47 | ATTR_TO_PROPERTY = [ 48 | ATTR_START, 49 | ATTR_END, 50 | ATTR_SOURCES, 51 | ATTR_COUNT_SOURCES, 52 | ATTR_AVAILABLE_SOURCES, 53 | ATTR_COUNT, 54 | ATTR_MAX_VALUE, 55 | ATTR_MIN_VALUE, 56 | ] 57 | 58 | 59 | UPDATE_MIN_TIME = timedelta(seconds=20) 60 | -------------------------------------------------------------------------------- /custom_components/average/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "average", 3 | "name": "Average Sensor", 4 | "version": "1.7.3", 5 | "documentation": "https://github.com/Limych/ha-average", 6 | "issue_tracker": "https://github.com/Limych/ha-average/issues", 7 | "dependencies": [ 8 | "history" 9 | ], 10 | "after_dependencies": [ 11 | "weather" 12 | ], 13 | "config_flow": false, 14 | "codeowners": [ 15 | "@Limych" 16 | ], 17 | "requirements": [] 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/beward/const.py: -------------------------------------------------------------------------------- 1 | """Constants for Beward component.""" 2 | from datetime import timedelta 3 | from typing import Dict 4 | 5 | from beward.const import ALARM_MOTION, ALARM_SENSOR 6 | from homeassistant.components.binary_sensor import ( 7 | DEVICE_CLASS_MOTION, 8 | DEVICE_CLASS_CONNECTIVITY, 9 | ) 10 | from homeassistant.const import DEVICE_CLASS_TIMESTAMP 11 | 12 | SUPPORT_LIB_URL = "https://github.com/Limych/py-beward/issues/new/choose" 13 | 14 | CONF_EVENTS = "events" 15 | CONF_RTSP_PORT = "rtsp_port" 16 | CONF_STREAM = "stream" 17 | CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" 18 | CONF_CAMERAS = "cameras" 19 | 20 | EVENT_ONLINE = "online" 21 | EVENT_MOTION = "motion" 22 | EVENT_DING = "ding" 23 | 24 | ALARMS_TO_EVENTS = { 25 | ALARM_MOTION: EVENT_MOTION, 26 | ALARM_SENSOR: EVENT_DING, 27 | } 28 | 29 | ATTR_DEVICE_ID = "device_id" 30 | 31 | CAT_DOORBELL = "doorbell" 32 | CAT_CAMERA = "camera" 33 | 34 | DEVICE_CHECK_INTERVAL = timedelta(seconds=15) 35 | 36 | CAMERA_LIVE = "live" 37 | CAMERA_LAST_MOTION = "last_motion" 38 | CAMERA_LAST_DING = "last_ding" 39 | 40 | CAMERA_NAME_LIVE = "{} Live" 41 | CAMERA_NAME_LAST_MOTION = "{} Last Motion" 42 | CAMERA_NAME_LAST_DING = "{} Last Ding" 43 | 44 | SENSOR_LAST_ACTIVITY = "last_activity" 45 | SENSOR_LAST_MOTION = "last_motion" 46 | SENSOR_LAST_DING = "last_ding" 47 | 48 | # Camera types are defined like: name template, device class, device event 49 | CAMERAS: Dict[str, list] = { 50 | CAMERA_LIVE: [CAMERA_NAME_LIVE, [CAT_DOORBELL, CAT_CAMERA], None], 51 | CAMERA_LAST_MOTION: [ 52 | CAMERA_NAME_LAST_MOTION, 53 | [CAT_DOORBELL, CAT_CAMERA], 54 | EVENT_MOTION, 55 | ], 56 | CAMERA_LAST_DING: [CAMERA_NAME_LAST_DING, [CAT_DOORBELL], EVENT_DING], 57 | } 58 | 59 | # Sensor types: name, category, class 60 | BINARY_SENSORS: Dict[str, list] = { 61 | EVENT_DING: ["Ding", [CAT_DOORBELL], None], 62 | EVENT_MOTION: ["Motion", [CAT_DOORBELL, CAT_CAMERA], DEVICE_CLASS_MOTION], 63 | EVENT_ONLINE: ["Online", [CAT_DOORBELL, CAT_CAMERA], DEVICE_CLASS_CONNECTIVITY], 64 | } 65 | 66 | # Sensor types: name, category, class, icon 67 | SENSORS = { 68 | SENSOR_LAST_ACTIVITY: [ 69 | "Last Activity", 70 | [CAT_DOORBELL, CAT_CAMERA], 71 | DEVICE_CLASS_TIMESTAMP, 72 | "history", 73 | ], 74 | SENSOR_LAST_MOTION: [ 75 | "Last Motion", 76 | [CAT_DOORBELL, CAT_CAMERA], 77 | DEVICE_CLASS_TIMESTAMP, 78 | "history", 79 | ], 80 | SENSOR_LAST_DING: ["Last Ding", [CAT_DOORBELL], DEVICE_CLASS_TIMESTAMP, "history"], 81 | } 82 | -------------------------------------------------------------------------------- /custom_components/beward/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "codeowners": [ 3 | "@Limych" 4 | ], 5 | "config_flow": false, 6 | "dependencies": [], 7 | "documentation": "https://github.com/Limych/ha-beward", 8 | "domain": "beward", 9 | "name": "Beward", 10 | "requirements": [ 11 | "beward~=1.0" 12 | ] 13 | } -------------------------------------------------------------------------------- /custom_components/car_wash/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Car Wash binary sensor. 3 | 4 | For more details about this platform, please refer to the documentation at 5 | https://github.com/Limych/ha-car_wash/ 6 | """ 7 | -------------------------------------------------------------------------------- /custom_components/car_wash/const.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019-2021, Andrey "Limych" Khrolenok 2 | # Creative Commons BY-NC-SA 4.0 International Public License 3 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 4 | """ 5 | The Car Wash binary sensor. 6 | 7 | For more details about this platform, please refer to the documentation at 8 | https://github.com/Limych/ha-car_wash/ 9 | """ 10 | 11 | from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR 12 | from homeassistant.components.weather import ( 13 | ATTR_CONDITION_EXCEPTIONAL, 14 | ATTR_CONDITION_HAIL, 15 | ATTR_CONDITION_LIGHTNING_RAINY, 16 | ATTR_CONDITION_POURING, 17 | ATTR_CONDITION_RAINY, 18 | ATTR_CONDITION_SNOWY, 19 | ATTR_CONDITION_SNOWY_RAINY, 20 | ) 21 | 22 | # Base component constants 23 | NAME = "Car Wash" 24 | DOMAIN = "car_wash" 25 | VERSION = "1.4.0" 26 | ISSUE_URL = "https://github.com/Limych/ha-car_wash/issues" 27 | 28 | STARTUP_MESSAGE = f""" 29 | ------------------------------------------------------------------- 30 | {NAME} 31 | Version: {VERSION} 32 | This is a custom integration! 33 | If you have ANY issues with this you need to open an issue here: 34 | {ISSUE_URL} 35 | ------------------------------------------------------------------- 36 | """ 37 | 38 | # Icons 39 | ICON = "mdi:car-wash" 40 | 41 | # Device classes 42 | 43 | # Platforms 44 | PLATFORMS = [BINARY_SENSOR] 45 | 46 | # Configuration and options 47 | CONF_WEATHER = "weather" 48 | CONF_DAYS = "days" 49 | 50 | # Defaults 51 | DEFAULT_NAME = "Car Wash" 52 | DEFAULT_DAYS = 2 53 | 54 | # Attributes 55 | 56 | 57 | BAD_CONDITIONS = [ 58 | ATTR_CONDITION_LIGHTNING_RAINY, 59 | ATTR_CONDITION_RAINY, 60 | ATTR_CONDITION_POURING, 61 | ATTR_CONDITION_SNOWY, 62 | ATTR_CONDITION_SNOWY_RAINY, 63 | ATTR_CONDITION_HAIL, 64 | ATTR_CONDITION_EXCEPTIONAL, 65 | ] 66 | -------------------------------------------------------------------------------- /custom_components/car_wash/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "car_wash", 3 | "name": "Car Wash", 4 | "version": "1.4.0", 5 | "documentation": "https://github.com/Limych/ha-car_wash", 6 | "issue_tracker": "https://github.com/Limych/ha-car_wash/issues", 7 | "dependencies": [ 8 | "weather" 9 | ], 10 | "config_flow": false, 11 | "codeowners": [ 12 | "@Limych" 13 | ], 14 | "requirements": [] 15 | } 16 | -------------------------------------------------------------------------------- /custom_components/car_wash/translations/binary_sensor.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "car_wash__": { 4 | "off": "Should not wash", 5 | "on": "Worth wash" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/car_wash/translations/binary_sensor.it.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "car_wash__": { 4 | "off": "Lavaggio sconsigliato", 5 | "on": "Procedi al lavaggio" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/car_wash/translations/binary_sensor.pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "car_wash__": { 4 | "off": "Nie należy myć", 5 | "on": "Warto umyć" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/car_wash/translations/binary_sensor.ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "car_wash__": { 4 | "off": "Нет смысла мыть", 5 | "on": "Стоит помыть" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/car_wash/translations/binary_sensor.uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "car_wash__": { 4 | "off": "Не варто мити", 5 | "on": "Варто помити" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/gismeteo/cache.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Vladimir Maksimenko 2 | # Copyright (c) 2019-2021, Andrey "Limych" Khrolenok 3 | # 4 | # Version 3.0.1 5 | """Cache controller.""" 6 | 7 | import logging 8 | import os 9 | import time 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class Cache: 15 | """Data caching class.""" 16 | 17 | def __init__(self, params=None): 18 | """Initialize cache.""" 19 | _LOGGER.debug("Initializing cache") 20 | params = params or {} 21 | 22 | self._cache_dir = params.get("cache_dir", "") 23 | self._cache_time = params.get("cache_time", 0) 24 | self._domain = params.get("domain") 25 | 26 | if self._cache_dir: 27 | self._cache_dir = os.path.abspath(self._cache_dir) 28 | 29 | if params.get("clean_dir", False): 30 | self._clean_dir() 31 | 32 | def _clean_dir(self): 33 | """Clean cache.""" 34 | now_time = time.time() 35 | 36 | if self._cache_dir and os.path.exists(self._cache_dir): 37 | _LOGGER.debug("Cleaning cache directory %s", self._cache_dir) 38 | files = os.listdir(self._cache_dir) 39 | _LOGGER.debug(files) 40 | for file_name in files: 41 | file_path = os.path.join(self._cache_dir, file_name) 42 | try: 43 | file_time = os.path.getmtime(file_path) 44 | if (file_time + self._cache_time) <= now_time: 45 | os.remove(file_path) 46 | except FileNotFoundError: # pragma: no cover 47 | pass 48 | 49 | def _get_file_path(self, file_name): 50 | """Get path of cache file.""" 51 | if self._domain: 52 | file_name = ".".join((self._domain, file_name)) 53 | return os.path.join(self._cache_dir, file_name) 54 | 55 | def is_cached(self, file_name): 56 | """Return True if cache file is exists.""" 57 | result = False 58 | 59 | file_path = self._get_file_path(file_name) 60 | if os.path.exists(file_path) and os.path.isfile(file_path): 61 | file_time = os.path.getmtime(file_path) 62 | now_time = time.time() 63 | 64 | result = (file_time + self._cache_time) > now_time 65 | 66 | return result 67 | 68 | def read_cache(self, file_name): 69 | """Read cached data.""" 70 | file_path = self._get_file_path(file_name) 71 | _LOGGER.debug("Read cache file %s", file_path) 72 | if self.is_cached(file_name): 73 | content = open(file_path).read() 74 | else: 75 | content = None 76 | 77 | return content 78 | 79 | def save_cache(self, file_name, content): 80 | """Save data to cache.""" 81 | if self._cache_dir: 82 | if not os.path.exists(self._cache_dir): 83 | os.makedirs(self._cache_dir) 84 | 85 | file_path = self._get_file_path(file_name) 86 | _LOGGER.debug("Store cache file %s", file_path) 87 | 88 | open(file_path, "w").write(content) 89 | -------------------------------------------------------------------------------- /custom_components/gismeteo/entity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019-2021, Andrey "Limych" Khrolenok 2 | # Creative Commons BY-NC-SA 4.0 International Public License 3 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 4 | """ 5 | The Gismeteo component. 6 | 7 | For more details about this platform, please refer to the documentation at 8 | https://github.com/Limych/ha-gismeteo/ 9 | """ 10 | 11 | from homeassistant.const import ATTR_ID 12 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 13 | 14 | from . import GismeteoDataUpdateCoordinator 15 | from .api import GismeteoApiClient 16 | from .const import DOMAIN, NAME 17 | 18 | 19 | class GismeteoEntity(CoordinatorEntity): 20 | """Gismeteo entity.""" 21 | 22 | def __init__(self, location_name: str, coordinator: GismeteoDataUpdateCoordinator): 23 | """Class initialization.""" 24 | super().__init__(coordinator) 25 | self._location_name = location_name 26 | 27 | @property 28 | def _gismeteo(self) -> GismeteoApiClient: 29 | return self.coordinator.gismeteo 30 | 31 | @property 32 | def device_info(self): 33 | """Return the device info.""" 34 | return { 35 | "identifiers": {(DOMAIN, self._gismeteo.attributes[ATTR_ID])}, 36 | "name": NAME, 37 | "manufacturer": NAME, 38 | "model": "Forecast", 39 | } 40 | -------------------------------------------------------------------------------- /custom_components/gismeteo/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "gismeteo", 3 | "name": "Gismeteo", 4 | "version": "2.3.1", 5 | "documentation": "https://github.com/Limych/ha-gismeteo", 6 | "issue_tracker": "https://github.com/Limych/ha-gismeteo/issues", 7 | "dependencies": [ 8 | "weather" 9 | ], 10 | "config_flow": true, 11 | "codeowners": [ 12 | "@Limych" 13 | ], 14 | "requirements": [] 15 | } 16 | -------------------------------------------------------------------------------- /custom_components/gismeteo/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "no_mixed_config": "Already configured. Configuration via configuration.yaml are not compatible with configurations via UI." 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect" 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "api_key": "API Key", 13 | "latitude": "Latitude", 14 | "longitude": "Longitude", 15 | "name": "Name" 16 | }, 17 | "description": "If you need help with the configuration have a look here: https://github.com/Limych/ha-gismeteo", 18 | "title": "Gismeteo" 19 | } 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "user": { 25 | "data": { 26 | "platform_sensor": "Sensor entities enabled", 27 | "platform_weather": "Weather entity enabled", 28 | "mode": "Forecast Mode", 29 | "forecast": "Add 3h Forecast Sensor" 30 | }, 31 | "title": "Gismeteo Options" 32 | } 33 | } 34 | }, 35 | "system_health": { 36 | "info": { 37 | "can_reach_server": "Reach Gismeteo server" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /custom_components/gismeteo/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "no_mixed_config": "Już skonfigurowane. Konfiguracja za pomocą configuration.yaml nie jest zgodna z konfiguracją za pośrednictwem interfejsu użytkownika." 5 | }, 6 | "error": { 7 | "cannot_connect": "Nie udało się połączyć" 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "api_key": "API Key", 13 | "latitude": "Szerokość geograficzna", 14 | "longitude": "Długość geograficzna", 15 | "name": "Nazwa" 16 | }, 17 | "description": "Jeśli potrzebujesz pomocy przy konfiguracji zajrzyj tutaj: https://github.com/Limych/ha-gismeteo", 18 | "title": "Gismeteo" 19 | } 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "user": { 25 | "data": { 26 | "platform_sensor": "Encje sensorów włączone", 27 | "platform_weather": "Encja pogody włączona", 28 | "mode": "Tryb prognozy", 29 | "forecast": "Dodaj 3-godzinny sensor prognozy" 30 | }, 31 | "title": "Opcje Gismeteo" 32 | } 33 | } 34 | }, 35 | "system_health": { 36 | "info": { 37 | "can_reach_server": "Dostęp do serwera Gismeteo" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /custom_components/gismeteo/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "no_mixed_config": "Настройка уже выполнена. Настройка через configuration.yaml несовместима с настройкой через интерфейс." 5 | }, 6 | "error": { 7 | "cannot_connect": "Не удалось подключиться." 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "api_key": "Ключ API", 13 | "latitude": "Широта", 14 | "longitude": "Долгота", 15 | "name": "Название" 16 | }, 17 | "description": "Ознакомьтесь с инструкциями, если Вам нужна помощь с настройкой:\nhttps://github.com/Limych/ha-gismeteo", 18 | "title": "Gismeteo" 19 | } 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "user": { 25 | "data": { 26 | "platform_sensor": "Сенсоры включены", 27 | "platform_weather": "Объект weather включен", 28 | "mode": "Режим прогноза", 29 | "forecast": "Добавить сенсор 3-часового прогноза" 30 | }, 31 | "title": "Настройки Gismeteo" 32 | } 33 | } 34 | }, 35 | "system_health": { 36 | "info": { 37 | "can_reach_server": "Доступ к серверу Gismeteo" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /custom_components/gismeteo/translations/sensor.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "gismeteo__condition": { 4 | "clear-night": "Clear, night", 5 | "cloudy": "Cloudy", 6 | "exceptional": "Exceptional", 7 | "fog": "Fog", 8 | "hail": "Hail", 9 | "lightning": "Lightning", 10 | "lightning-rainy": "Lightning, rainy", 11 | "partlycloudy": "Partly cloudy", 12 | "pouring": "Pouring", 13 | "rainy": "Rainy", 14 | "snowy": "Snowy", 15 | "snowy-rainy": "Snowy, rainy", 16 | "sunny": "Sunny", 17 | "windy": "Windy", 18 | "windy-variant": "Windy" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /custom_components/gismeteo/translations/sensor.pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "gismeteo__condition": { 4 | "clear-night": "Pogodna noc", 5 | "cloudy": "Pochmurno", 6 | "exceptional": "Warunki nadzwyczajne", 7 | "fog": "Mgła", 8 | "hail": "Grad", 9 | "lightning": "Błyskawice", 10 | "lightning-rainy": "Burza", 11 | "partlycloudy": "Częściowe zachmurzenie", 12 | "pouring": "Ulewa", 13 | "rainy": "Deszcz", 14 | "snowy": "Śnieg", 15 | "snowy-rainy": "Śnieg z deszczem", 16 | "sunny": "Słonecznie", 17 | "windy": "Wietrznie", 18 | "windy-variant": "Wietrznie" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /custom_components/gismeteo/translations/sensor.ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "gismeteo__condition": { 4 | "clear-night": "\u042f\u0441\u043d\u043e, \u043d\u043e\u0447\u044c", 5 | "cloudy": "\u041e\u0431\u043b\u0430\u0447\u043d\u043e", 6 | "exceptional": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435", 7 | "fog": "\u0422\u0443\u043c\u0430\u043d", 8 | "hail": "\u0413\u0440\u0430\u0434", 9 | "lightning": "\u041c\u043e\u043b\u043d\u0438\u044f", 10 | "lightning-rainy": "\u041c\u043e\u043b\u043d\u0438\u044f, \u0434\u043e\u0436\u0434\u044c", 11 | "partlycloudy": "\u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0441\u0442\u044c", 12 | "pouring": "\u041b\u0438\u0432\u0435\u043d\u044c", 13 | "rainy": "\u0414\u043e\u0436\u0434\u044c", 14 | "snowy": "\u0421\u043d\u0435\u0433", 15 | "snowy-rainy": "\u0421\u043d\u0435\u0433 \u0441 \u0434\u043e\u0436\u0434\u0435\u043c", 16 | "sunny": "\u042f\u0441\u043d\u043e", 17 | "windy": "\u0412\u0435\u0442\u0440\u0435\u043d\u043e", 18 | "windy-variant": "\u0412\u0435\u0442\u0440\u0435\u043d\u043e" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /custom_components/gismeteo/translations/sensor.uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "gismeteo__condition": { 4 | "clear-night": "\u042f\u0441\u043d\u043e, \u043d\u0456\u0447", 5 | "cloudy": "\u0425\u043c\u0430\u0440\u043d\u043e", 6 | "exceptional": "\u041f\u043e\u043f\u0435\u0440\u0435\u0434\u0436\u0435\u043d\u043d\u044f", 7 | "fog": "\u0422\u0443\u043c\u0430\u043d", 8 | "hail": "\u0413\u0440\u0430\u0434", 9 | "lightning": "\u0411\u043b\u0438\u0441\u043a\u0430\u0432\u043a\u0430", 10 | "lightning-rainy": "\u0411\u043b\u0438\u0441\u043a\u0430\u0432\u043a\u0430, \u0434\u043e\u0449", 11 | "partlycloudy": "\u041d\u0435\u0432\u0435\u043b\u0438\u043a\u0430 \u0445\u043c\u0430\u0440\u043d\u0456\u0441\u0442\u044c", 12 | "pouring": "\u0417\u043b\u0438\u0432\u0430", 13 | "rainy": "\u0414\u043e\u0449\u043e\u0432\u0430", 14 | "snowy": "\u0421\u043d\u0456\u0436\u043d\u043e", 15 | "snowy-rainy": "\u0421\u043d\u0456\u0433, \u0434\u043e\u0449", 16 | "sunny": "\u0421\u043e\u043d\u044f\u0447\u043d\u043e", 17 | "windy": "\u0412\u0456\u0442\u0440\u044f\u043d\u043e", 18 | "windy-variant": "\u0412\u0456\u0442\u0440\u044f\u043d\u043e" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /custom_components/hacs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | HACS gives you a powerful UI to handle downloads of all your custom needs. 3 | 4 | For more details about this integration, please refer to the documentation at 5 | https://hacs.xyz/ 6 | """ 7 | import voluptuous as vol 8 | 9 | from .const import DOMAIN 10 | from .helpers.functions.configuration_schema import hacs_config_combined 11 | from .operational.setup import async_setup as hacs_yaml_setup 12 | from .operational.setup import async_setup_entry as hacs_ui_setup 13 | from .operational.remove import async_remove_entry as hacs_remove_entry 14 | 15 | CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA) 16 | 17 | 18 | async def async_setup(hass, config): 19 | """Set up this integration using yaml.""" 20 | 21 | return await hacs_yaml_setup(hass, config) 22 | 23 | 24 | async def async_setup_entry(hass, config_entry): 25 | """Set up this integration using UI.""" 26 | 27 | return await hacs_ui_setup(hass, config_entry) 28 | 29 | 30 | async def async_remove_entry(hass, config_entry): 31 | """Handle removal of an entry.""" 32 | return await hacs_remove_entry(hass, config_entry) 33 | -------------------------------------------------------------------------------- /custom_components/hacs/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize HACS API""" 2 | -------------------------------------------------------------------------------- /custom_components/hacs/api/acknowledge_critical_repository.py: -------------------------------------------------------------------------------- 1 | """API Handler for acknowledge_critical_repository""" 2 | import homeassistant.helpers.config_validation as cv 3 | import voluptuous as vol 4 | from homeassistant.components import websocket_api 5 | 6 | from custom_components.hacs.helpers.functions.store import ( 7 | async_load_from_store, 8 | async_save_to_store, 9 | ) 10 | 11 | 12 | @websocket_api.async_response 13 | @websocket_api.websocket_command( 14 | {vol.Required("type"): "hacs/critical", vol.Optional("repository"): cv.string} 15 | ) 16 | async def acknowledge_critical_repository(hass, connection, msg): 17 | """Handle get media player cover command.""" 18 | repository = msg["repository"] 19 | 20 | critical = await async_load_from_store(hass, "critical") 21 | for repo in critical: 22 | if repository == repo["repository"]: 23 | repo["acknowledged"] = True 24 | await async_save_to_store(hass, "critical", critical) 25 | connection.send_message(websocket_api.result_message(msg["id"], critical)) 26 | -------------------------------------------------------------------------------- /custom_components/hacs/api/check_local_path.py: -------------------------------------------------------------------------------- 1 | """API Handler for check_local_path""" 2 | import homeassistant.helpers.config_validation as cv 3 | import voluptuous as vol 4 | from homeassistant.components import websocket_api 5 | 6 | from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist 7 | 8 | 9 | @websocket_api.async_response 10 | @websocket_api.websocket_command( 11 | {vol.Required("type"): "hacs/check_path", vol.Optional("path"): cv.string} 12 | ) 13 | async def check_local_path(_hass, connection, msg): 14 | """Handle get media player cover command.""" 15 | path = msg.get("path") 16 | exist = {"exist": False} 17 | 18 | if path is None: 19 | return 20 | 21 | if await async_path_exsist(path): 22 | exist["exist"] = True 23 | 24 | connection.send_message(websocket_api.result_message(msg["id"], exist)) 25 | -------------------------------------------------------------------------------- /custom_components/hacs/api/get_critical_repositories.py: -------------------------------------------------------------------------------- 1 | """API Handler for get_critical_repositories""" 2 | import voluptuous as vol 3 | from homeassistant.components import websocket_api 4 | 5 | from custom_components.hacs.helpers.functions.store import async_load_from_store 6 | 7 | 8 | @websocket_api.async_response 9 | @websocket_api.websocket_command({vol.Required("type"): "hacs/get_critical"}) 10 | async def get_critical_repositories(hass, connection, msg): 11 | """Handle get media player cover command.""" 12 | critical = await async_load_from_store(hass, "critical") 13 | if not critical: 14 | critical = [] 15 | connection.send_message(websocket_api.result_message(msg["id"], critical)) 16 | -------------------------------------------------------------------------------- /custom_components/hacs/api/hacs_config.py: -------------------------------------------------------------------------------- 1 | """API Handler for hacs_config""" 2 | import voluptuous as vol 3 | from homeassistant.components import websocket_api 4 | 5 | from custom_components.hacs.share import get_hacs 6 | 7 | 8 | @websocket_api.async_response 9 | @websocket_api.websocket_command({vol.Required("type"): "hacs/config"}) 10 | async def hacs_config(_hass, connection, msg): 11 | """Handle get media player cover command.""" 12 | hacs = get_hacs() 13 | config = hacs.configuration 14 | 15 | content = {} 16 | content["frontend_mode"] = config.frontend_mode 17 | content["frontend_compact"] = config.frontend_compact 18 | content["onboarding_done"] = config.onboarding_done 19 | content["version"] = hacs.version 20 | content["frontend_expected"] = hacs.frontend.version_expected 21 | content["frontend_running"] = hacs.frontend.version_running 22 | content["dev"] = config.dev 23 | content["debug"] = config.debug 24 | content["country"] = config.country 25 | content["experimental"] = config.experimental 26 | content["categories"] = hacs.common.categories 27 | 28 | connection.send_message(websocket_api.result_message(msg["id"], content)) 29 | -------------------------------------------------------------------------------- /custom_components/hacs/api/hacs_removed.py: -------------------------------------------------------------------------------- 1 | """API Handler for hacs_removed""" 2 | import voluptuous as vol 3 | from homeassistant.components import websocket_api 4 | 5 | from custom_components.hacs.share import list_removed_repositories 6 | 7 | 8 | @websocket_api.async_response 9 | @websocket_api.websocket_command({vol.Required("type"): "hacs/removed"}) 10 | async def hacs_removed(_hass, connection, msg): 11 | """Get information about removed repositories.""" 12 | content = [] 13 | for repo in list_removed_repositories(): 14 | content.append(repo.to_json()) 15 | connection.send_message(websocket_api.result_message(msg["id"], content)) 16 | -------------------------------------------------------------------------------- /custom_components/hacs/api/hacs_repositories.py: -------------------------------------------------------------------------------- 1 | """API Handler for hacs_repositories""" 2 | import voluptuous as vol 3 | from homeassistant.components import websocket_api 4 | 5 | from custom_components.hacs.share import get_hacs 6 | 7 | 8 | @websocket_api.async_response 9 | @websocket_api.websocket_command({vol.Required("type"): "hacs/repositories"}) 10 | async def hacs_repositories(_hass, connection, msg): 11 | """Handle get media player cover command.""" 12 | hacs = get_hacs() 13 | repositories = hacs.repositories 14 | content = [] 15 | for repo in repositories: 16 | if repo.data.category in hacs.common.categories: 17 | data = { 18 | "additional_info": repo.information.additional_info, 19 | "authors": repo.data.authors, 20 | "available_version": repo.display_available_version, 21 | "beta": repo.data.show_beta, 22 | "can_install": repo.can_install, 23 | "category": repo.data.category, 24 | "country": repo.data.country, 25 | "config_flow": repo.data.config_flow, 26 | "custom": repo.custom, 27 | "default_branch": repo.data.default_branch, 28 | "description": repo.data.description, 29 | "domain": repo.data.domain, 30 | "downloads": repo.data.downloads, 31 | "file_name": repo.data.file_name, 32 | "first_install": repo.status.first_install, 33 | "full_name": repo.data.full_name, 34 | "hide": repo.data.hide, 35 | "hide_default_branch": repo.data.hide_default_branch, 36 | "homeassistant": repo.data.homeassistant, 37 | "id": repo.data.id, 38 | "info": repo.information.info, 39 | "installed_version": repo.display_installed_version, 40 | "installed": repo.data.installed, 41 | "issues": repo.data.open_issues, 42 | "javascript_type": repo.information.javascript_type, 43 | "last_updated": repo.data.last_updated, 44 | "local_path": repo.content.path.local, 45 | "main_action": repo.main_action, 46 | "name": repo.display_name, 47 | "new": repo.data.new, 48 | "pending_upgrade": repo.pending_upgrade, 49 | "releases": repo.data.published_tags, 50 | "selected_tag": repo.data.selected_tag, 51 | "stars": repo.data.stargazers_count, 52 | "state": repo.state, 53 | "status_description": repo.display_status_description, 54 | "status": repo.display_status, 55 | "topics": repo.data.topics, 56 | "updated_info": repo.status.updated_info, 57 | "version_or_commit": repo.display_version_or_commit, 58 | } 59 | 60 | content.append(data) 61 | 62 | connection.send_message(websocket_api.result_message(msg["id"], content)) 63 | -------------------------------------------------------------------------------- /custom_components/hacs/api/hacs_settings.py: -------------------------------------------------------------------------------- 1 | """API Handler for hacs_settings""" 2 | import homeassistant.helpers.config_validation as cv 3 | import voluptuous as vol 4 | from homeassistant.components import websocket_api 5 | 6 | from custom_components.hacs.helpers.functions.logger import getLogger 7 | from custom_components.hacs.share import get_hacs 8 | 9 | _LOGGER = getLogger() 10 | 11 | 12 | @websocket_api.async_response 13 | @websocket_api.websocket_command( 14 | { 15 | vol.Required("type"): "hacs/settings", 16 | vol.Optional("action"): cv.string, 17 | vol.Optional("categories"): cv.ensure_list, 18 | } 19 | ) 20 | async def hacs_settings(hass, connection, msg): 21 | """Handle get media player cover command.""" 22 | hacs = get_hacs() 23 | 24 | action = msg["action"] 25 | _LOGGER.debug("WS action '%s'", action) 26 | 27 | if action == "set_fe_grid": 28 | hacs.configuration.frontend_mode = "Grid" 29 | 30 | elif action == "onboarding_done": 31 | hacs.configuration.onboarding_done = True 32 | 33 | elif action == "set_fe_table": 34 | hacs.configuration.frontend_mode = "Table" 35 | 36 | elif action == "set_fe_compact_true": 37 | hacs.configuration.frontend_compact = False 38 | 39 | elif action == "set_fe_compact_false": 40 | hacs.configuration.frontend_compact = True 41 | 42 | elif action == "clear_new": 43 | for repo in hacs.repositories: 44 | if repo.data.new and repo.data.category in msg.get("categories", []): 45 | _LOGGER.debug( 46 | "Clearing new flag from '%s'", 47 | repo.data.full_name, 48 | ) 49 | repo.data.new = False 50 | else: 51 | _LOGGER.error("WS action '%s' is not valid", action) 52 | hass.bus.async_fire("hacs/config", {}) 53 | await hacs.data.async_write() 54 | connection.send_message(websocket_api.result_message(msg["id"], {})) 55 | -------------------------------------------------------------------------------- /custom_components/hacs/api/hacs_status.py: -------------------------------------------------------------------------------- 1 | """API Handler for hacs_status""" 2 | import voluptuous as vol 3 | from homeassistant.components import websocket_api 4 | 5 | from custom_components.hacs.share import get_hacs 6 | 7 | 8 | @websocket_api.async_response 9 | @websocket_api.websocket_command({vol.Required("type"): "hacs/status"}) 10 | async def hacs_status(_hass, connection, msg): 11 | """Handle get media player cover command.""" 12 | hacs = get_hacs() 13 | content = { 14 | "startup": hacs.status.startup, 15 | "background_task": hacs.status.background_task, 16 | "lovelace_mode": hacs.system.lovelace_mode, 17 | "reloading_data": hacs.status.reloading_data, 18 | "upgrading_all": hacs.status.upgrading_all, 19 | "disabled": hacs.system.disabled, 20 | "disabled_reason": hacs.system.disabled_reason, 21 | "has_pending_tasks": hacs.queue.has_pending_tasks, 22 | "stage": hacs.stage, 23 | } 24 | connection.send_message(websocket_api.result_message(msg["id"], content)) 25 | -------------------------------------------------------------------------------- /custom_components/hacs/enums.py: -------------------------------------------------------------------------------- 1 | """Helper constants.""" 2 | # pylint: disable=missing-class-docstring 3 | from enum import Enum 4 | 5 | 6 | class HacsCategory(str, Enum): 7 | APPDAEMON = "appdaemon" 8 | INTEGRATION = "integration" 9 | LOVELACE = "lovelace" 10 | PLUGIN = "plugin" # Kept for legacy purposes 11 | NETDAEMON = "netdaemon" 12 | PYTHON_SCRIPT = "python_script" 13 | THEME = "theme" 14 | REMOVED = "removed" 15 | 16 | 17 | class LovelaceMode(str, Enum): 18 | """Lovelace Modes.""" 19 | 20 | STORAGE = "storage" 21 | AUTO = "auto" 22 | YAML = "yaml" 23 | 24 | 25 | class HacsStage(str, Enum): 26 | SETUP = "setup" 27 | STARTUP = "startup" 28 | WAITING = "waiting" 29 | RUNNING = "running" 30 | BACKGROUND = "background" 31 | 32 | 33 | class HacsSetupTask(str, Enum): 34 | WEBSOCKET = "WebSocket API" 35 | FRONTEND = "Frontend" 36 | SENSOR = "Sensor" 37 | HACS_REPO = "Hacs Repository" 38 | CATEGORIES = "Additional categories" 39 | CLEAR_STORAGE = "Clear storage" 40 | 41 | 42 | class HacsDisabledReason(str, Enum): 43 | RATE_LIMIT = "rate_limit" 44 | REMOVED = "removed" 45 | INVALID_TOKEN = "invalid_token" 46 | CONSTRAINS = "constrains" 47 | LOAD_HACS = "load_hacs" 48 | RESTORE = "restore" 49 | -------------------------------------------------------------------------------- /custom_components/hacs/hacsbase/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/custom_components/hacs/hacsbase/__init__.py -------------------------------------------------------------------------------- /custom_components/hacs/hacsbase/configuration.py: -------------------------------------------------------------------------------- 1 | """HACS Configuration.""" 2 | import attr 3 | 4 | from custom_components.hacs.helpers.classes.exceptions import HacsException 5 | from custom_components.hacs.helpers.functions.logger import getLogger 6 | 7 | _LOGGER = getLogger() 8 | 9 | 10 | @attr.s(auto_attribs=True) 11 | class Configuration: 12 | """Configuration class.""" 13 | 14 | # Main configuration: 15 | appdaemon_path: str = "appdaemon/apps/" 16 | appdaemon: bool = False 17 | netdaemon_path: str = "netdaemon/apps/" 18 | netdaemon: bool = False 19 | config: dict = {} 20 | config_entry: dict = {} 21 | config_type: str = None 22 | debug: bool = False 23 | dev: bool = False 24 | frontend_mode: str = "Grid" 25 | frontend_compact: bool = False 26 | frontend_repo: str = "" 27 | frontend_repo_url: str = "" 28 | options: dict = {} 29 | onboarding_done: bool = False 30 | plugin_path: str = "www/community/" 31 | python_script_path: str = "python_scripts/" 32 | python_script: bool = False 33 | sidepanel_icon: str = "hacs:hacs" 34 | sidepanel_title: str = "HACS" 35 | theme_path: str = "themes/" 36 | theme: bool = False 37 | token: str = None 38 | 39 | # Config options: 40 | country: str = "ALL" 41 | experimental: bool = False 42 | release_limit: int = 5 43 | 44 | def to_json(self) -> dict: 45 | """Return a dict representation of the configuration.""" 46 | return self.__dict__ 47 | 48 | def print(self) -> None: 49 | """Print the current configuration to the log.""" 50 | config = self.to_json() 51 | for key in config: 52 | if key in ["config", "config_entry", "options", "token"]: 53 | continue 54 | _LOGGER.debug("%s: %s", key, config[key]) 55 | 56 | @staticmethod 57 | def from_dict(configuration: dict, options: dict = None) -> None: 58 | """Set attributes from dicts.""" 59 | if isinstance(options, bool) or isinstance(configuration.get("options"), bool): 60 | raise HacsException("Configuration is not valid.") 61 | 62 | if options is None: 63 | options = {} 64 | 65 | if not configuration: 66 | raise HacsException("Configuration is not valid.") 67 | 68 | config = Configuration() 69 | 70 | config.config = configuration 71 | config.options = options 72 | 73 | for conf_type in [configuration, options]: 74 | for key in conf_type: 75 | setattr(config, key, conf_type[key]) 76 | 77 | return config 78 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | from custom_components.hacs.helpers.methods import ( 3 | HacsHelperMethods, 4 | RepositoryHelperMethods, 5 | ) 6 | from custom_components.hacs.helpers.properties import RepositoryHelperProperties 7 | 8 | 9 | class RepositoryHelpers( 10 | RepositoryHelperMethods, 11 | RepositoryHelperProperties, 12 | ): 13 | """Helper class for repositories""" 14 | 15 | 16 | class HacsHelpers(HacsHelperMethods): 17 | """Helper class for HACS""" 18 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/classes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/custom_components/hacs/helpers/classes/__init__.py -------------------------------------------------------------------------------- /custom_components/hacs/helpers/classes/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom Exceptions.""" 2 | 3 | 4 | class HacsException(Exception): 5 | """Super basic.""" 6 | 7 | 8 | class HacsRepositoryArchivedException(HacsException): 9 | """For repositories that are archived.""" 10 | 11 | 12 | class HacsNotModifiedException(HacsException): 13 | """For responses that are not modified.""" 14 | 15 | 16 | class HacsExpectedException(HacsException): 17 | """For stuff that are expected.""" 18 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/classes/manifest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manifest handling of a repository. 3 | 4 | https://hacs.xyz/docs/publish/start#hacsjson 5 | """ 6 | from typing import List 7 | 8 | import attr 9 | 10 | from custom_components.hacs.helpers.classes.exceptions import HacsException 11 | 12 | 13 | @attr.s(auto_attribs=True) 14 | class HacsManifest: 15 | """HacsManifest class.""" 16 | 17 | name: str = None 18 | content_in_root: bool = False 19 | zip_release: bool = False 20 | filename: str = None 21 | manifest: dict = {} 22 | hacs: str = None 23 | hide_default_branch: bool = False 24 | domains: List[str] = [] 25 | country: List[str] = [] 26 | homeassistant: str = None 27 | persistent_directory: str = None 28 | iot_class: str = None 29 | render_readme: bool = False 30 | 31 | @staticmethod 32 | def from_dict(manifest: dict): 33 | """Set attributes from dicts.""" 34 | if manifest is None: 35 | raise HacsException("Missing manifest data") 36 | 37 | manifest_data = HacsManifest() 38 | 39 | manifest_data.manifest = manifest 40 | 41 | for key in manifest: 42 | setattr(manifest_data, key, manifest[key]) 43 | return manifest_data 44 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/classes/removed.py: -------------------------------------------------------------------------------- 1 | """Object for removed repositories.""" 2 | import attr 3 | 4 | 5 | @attr.s(auto_attribs=True) 6 | class RemovedRepository: 7 | repository: str = None 8 | reason: str = None 9 | link: str = None 10 | removal_type: str = None # archived, not_compliant, critical, dev, broken 11 | acknowledged: bool = False 12 | 13 | def update_data(self, data: dict): 14 | """Update data of the repository.""" 15 | for key in data: 16 | if key in self.__dict__: 17 | setattr(self, key, data[key]) 18 | 19 | def to_json(self): 20 | """Return a JSON representation of the data.""" 21 | return attr.asdict(self) 22 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/classes/validate.py: -------------------------------------------------------------------------------- 1 | class Validate: 2 | """Validate.""" 3 | 4 | errors = [] 5 | 6 | @property 7 | def success(self): 8 | """Return bool if the validation was a success.""" 9 | if self.errors: 10 | return False 11 | return True 12 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/custom_components/hacs/helpers/functions/__init__.py -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/configuration_schema.py: -------------------------------------------------------------------------------- 1 | """HACS Configuration Schemas.""" 2 | # pylint: disable=dangerous-default-value 3 | import voluptuous as vol 4 | 5 | from custom_components.hacs.const import LOCALE 6 | 7 | # Configuration: 8 | TOKEN = "token" 9 | SIDEPANEL_TITLE = "sidepanel_title" 10 | SIDEPANEL_ICON = "sidepanel_icon" 11 | FRONTEND_REPO = "frontend_repo" 12 | FRONTEND_REPO_URL = "frontend_repo_url" 13 | APPDAEMON = "appdaemon" 14 | NETDAEMON = "netdaemon" 15 | 16 | # Options: 17 | COUNTRY = "country" 18 | DEBUG = "debug" 19 | RELEASE_LIMIT = "release_limit" 20 | EXPERIMENTAL = "experimental" 21 | 22 | # Config group 23 | PATH_OR_URL = "frontend_repo_path_or_url" 24 | 25 | 26 | def hacs_base_config_schema(config: dict = {}) -> dict: 27 | """Return a shcema configuration dict for HACS.""" 28 | if not config: 29 | config = { 30 | TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxx", 31 | } 32 | return { 33 | vol.Required(TOKEN, default=config.get(TOKEN)): str, 34 | } 35 | 36 | 37 | def hacs_config_option_schema(options: dict = {}) -> dict: 38 | """Return a shcema for HACS configuration options.""" 39 | if not options: 40 | options = { 41 | APPDAEMON: False, 42 | COUNTRY: "ALL", 43 | DEBUG: False, 44 | EXPERIMENTAL: False, 45 | NETDAEMON: False, 46 | RELEASE_LIMIT: 5, 47 | SIDEPANEL_ICON: "hacs:hacs", 48 | SIDEPANEL_TITLE: "HACS", 49 | FRONTEND_REPO: "", 50 | FRONTEND_REPO_URL: "", 51 | } 52 | return { 53 | vol.Optional(SIDEPANEL_TITLE, default=options.get(SIDEPANEL_TITLE)): str, 54 | vol.Optional(SIDEPANEL_ICON, default=options.get(SIDEPANEL_ICON)): str, 55 | vol.Optional(RELEASE_LIMIT, default=options.get(RELEASE_LIMIT)): int, 56 | vol.Optional(COUNTRY, default=options.get(COUNTRY)): vol.In(LOCALE), 57 | vol.Optional(APPDAEMON, default=options.get(APPDAEMON)): bool, 58 | vol.Optional(NETDAEMON, default=options.get(NETDAEMON)): bool, 59 | vol.Optional(DEBUG, default=options.get(DEBUG)): bool, 60 | vol.Optional(EXPERIMENTAL, default=options.get(EXPERIMENTAL)): bool, 61 | vol.Exclusive(FRONTEND_REPO, PATH_OR_URL): str, 62 | vol.Exclusive(FRONTEND_REPO_URL, PATH_OR_URL): str, 63 | } 64 | 65 | 66 | def hacs_config_combined() -> dict: 67 | """Combine the configuration options.""" 68 | base = hacs_base_config_schema() 69 | options = hacs_config_option_schema() 70 | 71 | for option in options: 72 | base[option] = options[option] 73 | 74 | return base 75 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/constrains.py: -------------------------------------------------------------------------------- 1 | """HACS Startup constrains.""" 2 | # pylint: disable=bad-continuation 3 | import os 4 | 5 | from custom_components.hacs.const import ( 6 | CUSTOM_UPDATER_LOCATIONS, 7 | CUSTOM_UPDATER_WARNING, 8 | MINIMUM_HA_VERSION, 9 | ) 10 | from custom_components.hacs.helpers.functions.misc import version_left_higher_then_right 11 | from custom_components.hacs.share import get_hacs 12 | 13 | 14 | def check_constrains(): 15 | """Check HACS constrains.""" 16 | if not constrain_custom_updater(): 17 | return False 18 | if not constrain_version(): 19 | return False 20 | return True 21 | 22 | 23 | def constrain_custom_updater(): 24 | """Check if custom_updater exist.""" 25 | hacs = get_hacs() 26 | for location in CUSTOM_UPDATER_LOCATIONS: 27 | if os.path.exists(location.format(hacs.core.config_path)): 28 | msg = CUSTOM_UPDATER_WARNING.format(location.format(hacs.core.config_path)) 29 | hacs.log.critical(msg) 30 | return False 31 | return True 32 | 33 | 34 | def constrain_version(): 35 | """Check if the version is valid.""" 36 | hacs = get_hacs() 37 | if not version_left_higher_then_right(hacs.system.ha_version, MINIMUM_HA_VERSION): 38 | hacs.log.critical( 39 | "You need HA version %s or newer to use this integration.", 40 | MINIMUM_HA_VERSION, 41 | ) 42 | return False 43 | return True 44 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/filters.py: -------------------------------------------------------------------------------- 1 | """Filter functions.""" 2 | 3 | 4 | def filter_content_return_one_of_type( 5 | content, namestartswith, filterfiltype, attr="name" 6 | ): 7 | """Only match 1 of the filter.""" 8 | contents = [] 9 | filetypefound = False 10 | for filename in content: 11 | if isinstance(filename, str): 12 | if filename.startswith(namestartswith): 13 | if filename.endswith(f".{filterfiltype}"): 14 | if not filetypefound: 15 | contents.append(filename) 16 | filetypefound = True 17 | continue 18 | else: 19 | contents.append(filename) 20 | else: 21 | if getattr(filename, attr).startswith(namestartswith): 22 | if getattr(filename, attr).endswith(f".{filterfiltype}"): 23 | if not filetypefound: 24 | contents.append(filename) 25 | filetypefound = True 26 | continue 27 | else: 28 | contents.append(filename) 29 | return contents 30 | 31 | 32 | def find_first_of_filetype(content, filterfiltype, attr="name"): 33 | """Find the first of the file type.""" 34 | filename = "" 35 | for _filename in content: 36 | if isinstance(_filename, str): 37 | if _filename.endswith(f".{filterfiltype}"): 38 | filename = _filename 39 | break 40 | else: 41 | if getattr(_filename, attr).endswith(f".{filterfiltype}"): 42 | filename = getattr(_filename, attr) 43 | break 44 | return filename 45 | 46 | 47 | def get_first_directory_in_directory(content, dirname): 48 | """Return the first directory in dirname or None.""" 49 | directory = None 50 | for path in content: 51 | if path.full_path.startswith(dirname) and path.full_path != dirname: 52 | if path.is_directory: 53 | directory = path.filename 54 | break 55 | return directory 56 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/get_list_from_default.py: -------------------------------------------------------------------------------- 1 | """Helper to get default repositories.""" 2 | import json 3 | from typing import List 4 | 5 | from aiogithubapi import AIOGitHubAPIException 6 | 7 | from custom_components.hacs.enums import HacsCategory 8 | from custom_components.hacs.helpers.classes.exceptions import HacsException 9 | from custom_components.hacs.share import get_hacs 10 | 11 | 12 | async def async_get_list_from_default(default: HacsCategory) -> List: 13 | """Get repositories from default list.""" 14 | hacs = get_hacs() 15 | repositories = [] 16 | 17 | try: 18 | content = await hacs.data_repo.get_contents( 19 | default, hacs.data_repo.default_branch 20 | ) 21 | repositories = json.loads(content.content) 22 | 23 | except (AIOGitHubAPIException, HacsException) as exception: 24 | hacs.log.error(exception) 25 | 26 | except (Exception, BaseException) as exception: 27 | hacs.log.error(exception) 28 | 29 | hacs.log.debug("Got %s elements for %s", len(repositories), default) 30 | 31 | return repositories 32 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/is_safe_to_remove.py: -------------------------------------------------------------------------------- 1 | """Helper to check if path is safe to remove.""" 2 | from pathlib import Path 3 | 4 | from custom_components.hacs.share import get_hacs 5 | 6 | 7 | def is_safe_to_remove(path: str) -> bool: 8 | """Helper to check if path is safe to remove.""" 9 | hacs = get_hacs() 10 | paths = [ 11 | Path(f"{hacs.core.config_path}/{hacs.configuration.appdaemon_path}"), 12 | Path(f"{hacs.core.config_path}/{hacs.configuration.netdaemon_path}"), 13 | Path(f"{hacs.core.config_path}/{hacs.configuration.plugin_path}"), 14 | Path(f"{hacs.core.config_path}/{hacs.configuration.python_script_path}"), 15 | Path(f"{hacs.core.config_path}/{hacs.configuration.theme_path}"), 16 | Path(f"{hacs.core.config_path}/custom_components/"), 17 | ] 18 | if Path(path) in paths: 19 | return False 20 | return True 21 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/logger.py: -------------------------------------------------------------------------------- 1 | """Custom logger for HACS.""" 2 | # pylint: disable=invalid-name 3 | import logging 4 | import os 5 | 6 | from ...const import PACKAGE_NAME 7 | 8 | _HACSLogger: logging.Logger = logging.getLogger(PACKAGE_NAME) 9 | 10 | if "GITHUB_ACTION" in os.environ: 11 | logging.basicConfig( 12 | format="::%(levelname)s:: %(message)s", 13 | level="DEBUG", 14 | ) 15 | 16 | 17 | def getLogger(_name: str = None) -> logging.Logger: 18 | """Return a Logger instance.""" 19 | return _HACSLogger 20 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/misc.py: -------------------------------------------------------------------------------- 1 | """Helper functions: misc""" 2 | import re 3 | from functools import lru_cache 4 | 5 | from awesomeversion import AwesomeVersion 6 | 7 | RE_REPOSITORY = re.compile( 8 | r"(?:(?:.*github.com.)|^)([A-Za-z0-9-]+\/[\w.-]+?)(?:(?:\.git)?|(?:[^\w.-].*)?)$" 9 | ) 10 | 11 | 12 | def get_repository_name(repository) -> str: 13 | """Return the name of the repository for use in the frontend.""" 14 | 15 | if repository.repository_manifest.name is not None: 16 | return repository.repository_manifest.name 17 | 18 | if repository.data.category == "integration": 19 | if repository.integration_manifest: 20 | if "name" in repository.integration_manifest: 21 | return repository.integration_manifest["name"] 22 | 23 | return ( 24 | repository.data.full_name.split("/")[-1] 25 | .replace("-", " ") 26 | .replace("_", " ") 27 | .title() 28 | ) 29 | 30 | 31 | @lru_cache(maxsize=1024) 32 | def version_left_higher_then_right(left: str, right: str) -> bool: 33 | """Return a bool if source is newer than target, will also be true if identical.""" 34 | return AwesomeVersion(left) >= AwesomeVersion(right) 35 | 36 | 37 | def extract_repository_from_url(url: str) -> str or None: 38 | """Extract the owner/repo part form a URL.""" 39 | match = re.match(RE_REPOSITORY, url) 40 | if not match: 41 | return None 42 | return match.group(1).lower() 43 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/path_exsist.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | import os 3 | 4 | from custom_components.hacs.share import get_hacs 5 | 6 | 7 | def path_exsist(path) -> bool: 8 | return os.path.exists(path) 9 | 10 | 11 | async def async_path_exsist(path) -> bool: 12 | hass = get_hacs().hass 13 | return await hass.async_add_executor_job(path_exsist, path) 14 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/register_repository.py: -------------------------------------------------------------------------------- 1 | """Register a repository.""" 2 | from aiogithubapi import AIOGitHubAPIException 3 | 4 | from custom_components.hacs.helpers.classes.exceptions import ( 5 | HacsException, 6 | HacsExpectedException, 7 | ) 8 | from custom_components.hacs.share import get_hacs 9 | 10 | from ...repositories import RERPOSITORY_CLASSES 11 | 12 | 13 | # @concurrent(15, 5) 14 | async def register_repository(full_name, category, check=True, ref=None): 15 | """Register a repository.""" 16 | hacs = get_hacs() 17 | 18 | if full_name in hacs.common.skip: 19 | if full_name != "hacs/integration": 20 | raise HacsExpectedException(f"Skipping {full_name}") 21 | 22 | if category not in RERPOSITORY_CLASSES: 23 | raise HacsException(f"{category} is not a valid repository category.") 24 | 25 | repository = RERPOSITORY_CLASSES[category](full_name) 26 | if check: 27 | try: 28 | await repository.async_registration(ref) 29 | if hacs.status.new: 30 | repository.data.new = False 31 | if repository.validate.errors: 32 | hacs.common.skip.append(repository.data.full_name) 33 | if not hacs.status.startup: 34 | hacs.log.error("Validation for %s failed.", full_name) 35 | if hacs.system.action: 36 | raise HacsException(f"::error:: Validation for {full_name} failed.") 37 | return repository.validate.errors 38 | if hacs.system.action: 39 | repository.logger.info("%s Validation completed", repository) 40 | else: 41 | repository.logger.info("%s Registration completed", repository) 42 | except AIOGitHubAPIException as exception: 43 | hacs.common.skip.append(repository.data.full_name) 44 | raise HacsException( 45 | f"Validation for {full_name} failed with {exception}." 46 | ) from None 47 | 48 | exists = ( 49 | False 50 | if str(repository.data.id) == "0" 51 | else [x for x in hacs.repositories if str(x.data.id) == str(repository.data.id)] 52 | ) 53 | 54 | if exists: 55 | if exists[0] in hacs.repositories: 56 | hacs.repositories.remove(exists[0]) 57 | 58 | else: 59 | if hacs.hass is not None and ( 60 | (check and repository.data.new) or hacs.status.new 61 | ): 62 | hacs.hass.bus.async_fire( 63 | "hacs/repository", 64 | { 65 | "action": "registration", 66 | "repository": repository.data.full_name, 67 | "repository_id": repository.data.id, 68 | }, 69 | ) 70 | hacs.repositories.append(repository) 71 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/remaining_github_calls.py: -------------------------------------------------------------------------------- 1 | """Helper to calculate the remaining calls to github.""" 2 | import math 3 | 4 | from custom_components.hacs.helpers.functions.logger import getLogger 5 | 6 | _LOGGER = getLogger() 7 | 8 | 9 | async def remaining(github): 10 | """Helper to calculate the remaining calls to github.""" 11 | try: 12 | ratelimits = await github.get_rate_limit() 13 | except (BaseException, Exception) as exception: # pylint: disable=broad-except 14 | _LOGGER.error(exception) 15 | return None 16 | if ratelimits.get("remaining") is not None: 17 | return int(ratelimits["remaining"]) 18 | return 0 19 | 20 | 21 | async def get_fetch_updates_for(github): 22 | """Helper to calculate the number of repositories we can fetch data for.""" 23 | margin = 1000 24 | limit = await remaining(github) 25 | pr_repo = 15 26 | 27 | if limit is None: 28 | return None 29 | 30 | if limit - margin <= pr_repo: 31 | return 0 32 | return math.floor((limit - margin) / pr_repo) 33 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/save.py: -------------------------------------------------------------------------------- 1 | """Download.""" 2 | import gzip 3 | import os 4 | import shutil 5 | 6 | import aiofiles 7 | 8 | from custom_components.hacs.helpers.functions.logger import getLogger 9 | 10 | _LOGGER = getLogger() 11 | 12 | 13 | async def async_save_file(location, content): 14 | """Save files.""" 15 | _LOGGER.debug("Saving %s", location) 16 | mode = "w" 17 | encoding = "utf-8" 18 | errors = "ignore" 19 | 20 | if not isinstance(content, str): 21 | mode = "wb" 22 | encoding = None 23 | errors = None 24 | 25 | try: 26 | async with aiofiles.open( 27 | location, mode=mode, encoding=encoding, errors=errors 28 | ) as outfile: 29 | await outfile.write(content) 30 | outfile.close() 31 | 32 | # Create gz for .js files 33 | if os.path.isfile(location): 34 | if location.endswith(".js") or location.endswith(".css"): 35 | with open(location, "rb") as f_in: 36 | with gzip.open(location + ".gz", "wb") as f_out: 37 | shutil.copyfileobj(f_in, f_out) 38 | 39 | # Remove with 2.0 40 | if "themes" in location and location.endswith(".yaml"): 41 | filename = location.split("/")[-1] 42 | base = location.split("/themes/")[0] 43 | combined = f"{base}/themes/{filename}" 44 | if os.path.exists(combined): 45 | _LOGGER.info("Removing old theme file %s", combined) 46 | os.remove(combined) 47 | 48 | except (Exception, BaseException) as error: # pylint: disable=broad-except 49 | _LOGGER.error("Could not write data to %s - %s", location, error) 50 | return False 51 | 52 | return os.path.exists(location) 53 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/store.py: -------------------------------------------------------------------------------- 1 | """Storage handers.""" 2 | # pylint: disable=import-outside-toplevel 3 | from homeassistant.helpers.json import JSONEncoder 4 | 5 | from custom_components.hacs.const import VERSION_STORAGE 6 | from .logger import getLogger 7 | 8 | _LOGGER = getLogger() 9 | 10 | 11 | def get_store_for_key(hass, key): 12 | """Create a Store object for the key.""" 13 | key = key if "/" in key else f"hacs.{key}" 14 | from homeassistant.helpers.storage import Store 15 | 16 | return Store(hass, VERSION_STORAGE, key, encoder=JSONEncoder) 17 | 18 | 19 | async def async_load_from_store(hass, key): 20 | """Load the retained data from store and return de-serialized data.""" 21 | store = get_store_for_key(hass, key) 22 | restored = await store.async_load() 23 | if restored is None: 24 | return {} 25 | return restored 26 | 27 | 28 | async def async_save_to_store(hass, key, data): 29 | """Generate dynamic data to store and save it to the filesystem.""" 30 | current = await async_load_from_store(hass, key) 31 | if current is None or current != data: 32 | await get_store_for_key(hass, key).async_save(data) 33 | return 34 | _LOGGER.debug( 35 | "Did not store data for '%s'. Content did not change", 36 | key if "/" in key else f"hacs.{key}", 37 | ) 38 | 39 | 40 | async def async_remove_store(hass, key): 41 | """Remove a store element that should no longer be used""" 42 | if "/" not in key: 43 | return 44 | await get_store_for_key(hass, key).async_remove() 45 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/template.py: -------------------------------------------------------------------------------- 1 | """Custom template support.""" 2 | # pylint: disable=broad-except 3 | from jinja2 import Template 4 | 5 | from custom_components.hacs.helpers.functions.logger import getLogger 6 | 7 | _LOGGER = getLogger() 8 | 9 | 10 | def render_template(content, context): 11 | """Render templates in content.""" 12 | # Fix None issues 13 | if context.releases.last_release_object is not None: 14 | prerelease = context.releases.last_release_object.prerelease 15 | else: 16 | prerelease = False 17 | 18 | # Render the template 19 | try: 20 | render = Template(content) 21 | render = render.render( 22 | installed=context.data.installed, 23 | pending_update=context.pending_upgrade, 24 | prerelease=prerelease, 25 | selected_tag=context.data.selected_tag, 26 | version_available=context.releases.last_release, 27 | version_installed=context.display_installed_version, 28 | ) 29 | return render 30 | except (Exception, BaseException) as exception: 31 | _LOGGER.debug(exception) 32 | return content 33 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/functions/version_to_install.py: -------------------------------------------------------------------------------- 1 | """Install helper for repositories.""" 2 | 3 | 4 | def version_to_install(repository): 5 | """Determine which version to isntall.""" 6 | if repository.data.last_version is not None: 7 | if repository.data.selected_tag is not None: 8 | if repository.data.selected_tag == repository.data.last_version: 9 | repository.data.selected_tag = None 10 | return repository.data.last_version 11 | return repository.data.selected_tag 12 | return repository.data.last_version 13 | if repository.data.selected_tag is not None: 14 | if repository.data.selected_tag == repository.data.default_branch: 15 | return repository.data.default_branch 16 | if repository.data.selected_tag in repository.data.published_tags: 17 | return repository.data.selected_tag 18 | if repository.data.default_branch is None: 19 | return "main" 20 | return repository.data.default_branch 21 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/methods/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | from custom_components.hacs.helpers.methods.installation import ( 3 | RepositoryMethodInstall, 4 | RepositoryMethodPostInstall, 5 | RepositoryMethodPreInstall, 6 | ) 7 | from custom_components.hacs.helpers.methods.registration import ( 8 | RepositoryMethodPostRegistration, 9 | RepositoryMethodPreRegistration, 10 | RepositoryMethodRegistration, 11 | ) 12 | from custom_components.hacs.helpers.methods.reinstall_if_needed import ( 13 | RepositoryMethodReinstallIfNeeded, 14 | ) 15 | 16 | 17 | class RepositoryHelperMethods( 18 | RepositoryMethodReinstallIfNeeded, 19 | RepositoryMethodInstall, 20 | RepositoryMethodPostInstall, 21 | RepositoryMethodPreInstall, 22 | RepositoryMethodPreRegistration, 23 | RepositoryMethodRegistration, 24 | RepositoryMethodPostRegistration, 25 | ): 26 | """Collection of repository methods that are nested to all repositories.""" 27 | 28 | 29 | class HacsHelperMethods: 30 | """Helper class for HACS methods""" 31 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/methods/registration.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member, attribute-defined-outside-init 2 | from abc import ABC 3 | 4 | from custom_components.hacs.validate import async_run_repository_checks 5 | 6 | 7 | class RepositoryMethodPreRegistration(ABC): 8 | async def async_pre_registration(self): 9 | pass 10 | 11 | 12 | class RepositoryMethodRegistration(ABC): 13 | async def registration(self, ref=None) -> None: 14 | self.logger.warning( 15 | "'registration' is deprecated, use 'async_registration' instead" 16 | ) 17 | await self.async_registration(ref) 18 | 19 | async def async_registration(self, ref=None) -> None: 20 | # Run local pre registration steps. 21 | await self.async_pre_registration() 22 | 23 | if ref is not None: 24 | self.data.selected_tag = ref 25 | self.ref = ref 26 | self.force_branch = True 27 | 28 | if not await self.validate_repository(): 29 | return False 30 | 31 | # Run common registration steps. 32 | await self.common_registration() 33 | 34 | # Set correct local path 35 | self.content.path.local = self.localpath 36 | 37 | # Run local post registration steps. 38 | await self.async_post_registration() 39 | 40 | 41 | class RepositoryMethodPostRegistration(ABC): 42 | async def async_post_registration(self): 43 | await async_run_repository_checks(self) 44 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/methods/reinstall_if_needed.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | from abc import ABC 3 | 4 | from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist 5 | 6 | 7 | class RepositoryMethodReinstallIfNeeded(ABC): 8 | async def async_reinstall_if_needed(self) -> None: 9 | if self.data.installed: 10 | if not await async_path_exsist(self.content.path.local): 11 | self.logger.error("Missing from local FS, should be reinstalled.") 12 | # await self.async_install() 13 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/properties/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | from custom_components.hacs.helpers.properties.can_be_installed import ( 3 | RepositoryPropertyCanBeInstalled, 4 | ) 5 | from custom_components.hacs.helpers.properties.custom import RepositoryPropertyCustom 6 | from custom_components.hacs.helpers.properties.pending_update import ( 7 | RepositoryPropertyPendingUpdate, 8 | ) 9 | 10 | 11 | class RepositoryHelperProperties( 12 | RepositoryPropertyPendingUpdate, 13 | RepositoryPropertyCustom, 14 | RepositoryPropertyCanBeInstalled, 15 | ): 16 | pass 17 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/properties/can_be_installed.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | from abc import ABC 3 | 4 | from custom_components.hacs.helpers.functions.misc import version_left_higher_then_right 5 | 6 | 7 | class RepositoryPropertyCanBeInstalled(ABC): 8 | @property 9 | def can_be_installed(self) -> bool: 10 | if self.data.homeassistant is not None: 11 | if self.data.releases: 12 | if not version_left_higher_then_right( 13 | self.hacs.system.ha_version, self.data.homeassistant 14 | ): 15 | return False 16 | return True 17 | 18 | @property 19 | def can_install(self): 20 | """kept for legacy compatibility""" 21 | return self.can_be_installed 22 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/properties/custom.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | from abc import ABC 3 | 4 | 5 | class RepositoryPropertyCustom(ABC): 6 | @property 7 | def custom(self): 8 | """Return flag if the repository is custom.""" 9 | if str(self.data.id) in self.hacs.common.default: 10 | return False 11 | if self.data.full_name == "hacs/integration": 12 | return False 13 | return True 14 | -------------------------------------------------------------------------------- /custom_components/hacs/helpers/properties/pending_update.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | from abc import ABC 3 | 4 | 5 | class RepositoryPropertyPendingUpdate(ABC): 6 | @property 7 | def pending_update(self) -> bool: 8 | if not self.can_install: 9 | return False 10 | if self.data.installed: 11 | if self.data.selected_tag is not None: 12 | if self.data.selected_tag == self.data.default_branch: 13 | if self.data.installed_commit != self.data.last_commit: 14 | return True 15 | return False 16 | if self.display_installed_version != self.display_available_version: 17 | return True 18 | return False 19 | 20 | @property 21 | def pending_upgrade(self) -> bool: 22 | """kept for legacy compatibility""" 23 | return self.pending_update 24 | -------------------------------------------------------------------------------- /custom_components/hacs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "codeowners": [ 3 | "@ludeeus" 4 | ], 5 | "config_flow": true, 6 | "dependencies": [ 7 | "http", 8 | "websocket_api", 9 | "frontend", 10 | "persistent_notification", 11 | "lovelace" 12 | ], 13 | "documentation": "https://hacs.xyz/docs/configuration/start", 14 | "domain": "hacs", 15 | "issue_tracker": "https://github.com/hacs/integration/issues", 16 | "name": "HACS", 17 | "requirements": [ 18 | "aiofiles>=0.6.0", 19 | "aiogithubapi>=21.4.0", 20 | "awesomeversion>=21.2.2", 21 | "backoff>=1.10.0", 22 | "hacs_frontend==20210214181913", 23 | "queueman==0.5" 24 | ], 25 | "version": "1.12.1" 26 | } -------------------------------------------------------------------------------- /custom_components/hacs/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Hacs models.""" 2 | -------------------------------------------------------------------------------- /custom_components/hacs/models/core.py: -------------------------------------------------------------------------------- 1 | """HACS Core info.""" 2 | from pathlib import Path 3 | 4 | import attr 5 | 6 | from ..enums import LovelaceMode 7 | 8 | 9 | @attr.s 10 | class HacsCore: 11 | """HACS Core info.""" 12 | 13 | config_path = attr.ib(Path) 14 | ha_version = attr.ib(str) 15 | lovelace_mode = LovelaceMode("storage") 16 | -------------------------------------------------------------------------------- /custom_components/hacs/models/frontend.py: -------------------------------------------------------------------------------- 1 | """HacsFrontend.""" 2 | 3 | 4 | class HacsFrontend: 5 | """HacsFrontend.""" 6 | 7 | version_running: bool = None 8 | version_available: bool = None 9 | version_expected: bool = None 10 | update_pending: bool = False 11 | -------------------------------------------------------------------------------- /custom_components/hacs/models/system.py: -------------------------------------------------------------------------------- 1 | """HACS System info.""" 2 | from typing import Optional 3 | import attr 4 | 5 | from ..const import INTEGRATION_VERSION 6 | from ..enums import HacsStage 7 | 8 | 9 | @attr.s 10 | class HacsSystem: 11 | """HACS System info.""" 12 | 13 | disabled: bool = False 14 | disabled_reason: Optional[str] = None 15 | running: bool = False 16 | version: str = INTEGRATION_VERSION 17 | stage: HacsStage = attr.ib(HacsStage) 18 | action: bool = False 19 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/custom_components/hacs/operational/__init__.py -------------------------------------------------------------------------------- /custom_components/hacs/operational/factory.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring,invalid-name 2 | import asyncio 3 | 4 | from aiogithubapi import AIOGitHubAPIException 5 | 6 | from custom_components.hacs.helpers.classes.exceptions import ( 7 | HacsException, 8 | HacsNotModifiedException, 9 | HacsRepositoryArchivedException, 10 | ) 11 | from custom_components.hacs.helpers.functions.logger import getLogger 12 | from custom_components.hacs.helpers.functions.register_repository import ( 13 | register_repository, 14 | ) 15 | 16 | max_concurrent_tasks = asyncio.Semaphore(15) 17 | sleeper = 5 18 | 19 | _LOGGER = getLogger() 20 | 21 | 22 | class HacsTaskFactory: 23 | def __init__(self): 24 | self.tasks = [] 25 | self.running = False 26 | 27 | async def safe_common_update(self, repository): 28 | async with max_concurrent_tasks: 29 | try: 30 | await repository.common_update() 31 | except HacsNotModifiedException: 32 | pass 33 | except (AIOGitHubAPIException, HacsException) as exception: 34 | _LOGGER.error("%s - %s", repository.data.full_name, exception) 35 | 36 | # Due to GitHub ratelimits we need to sleep a bit 37 | await asyncio.sleep(sleeper) 38 | 39 | async def safe_update(self, repository): 40 | async with max_concurrent_tasks: 41 | try: 42 | await repository.update_repository() 43 | except HacsNotModifiedException: 44 | pass 45 | except HacsRepositoryArchivedException as exception: 46 | _LOGGER.warning("%s - %s", repository.data.full_name, exception) 47 | except (AIOGitHubAPIException, HacsException) as exception: 48 | _LOGGER.error("%s - %s", repository.data.full_name, exception) 49 | 50 | # Due to GitHub ratelimits we need to sleep a bit 51 | await asyncio.sleep(sleeper) 52 | 53 | async def safe_register(self, repo, category): 54 | async with max_concurrent_tasks: 55 | try: 56 | await register_repository(repo, category) 57 | except (AIOGitHubAPIException, HacsException) as exception: 58 | _LOGGER.error("%s - %s", repo, exception) 59 | 60 | # Due to GitHub ratelimits we need to sleep a bit 61 | await asyncio.sleep(sleeper) 62 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/reload.py: -------------------------------------------------------------------------------- 1 | """Reload HACS""" 2 | 3 | 4 | async def async_reload_entry(hass, config_entry): 5 | """Reload HACS.""" 6 | from custom_components.hacs.operational.remove import async_remove_entry 7 | from custom_components.hacs.operational.setup import async_setup_entry 8 | 9 | await async_remove_entry(hass, config_entry) 10 | await async_setup_entry(hass, config_entry) 11 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/remove.py: -------------------------------------------------------------------------------- 1 | """Remove HACS.""" 2 | from ..const import DOMAIN 3 | from ..enums import HacsDisabledReason 4 | from ..share import get_hacs 5 | 6 | 7 | async def async_remove_entry(hass, config_entry): 8 | """Handle removal of an entry.""" 9 | hacs = get_hacs() 10 | hacs.log.info("Disabling HACS") 11 | hacs.log.info("Removing recurring tasks") 12 | for task in hacs.recuring_tasks: 13 | task() 14 | if config_entry.state == "loaded": 15 | hacs.log.info("Removing sensor") 16 | try: 17 | await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") 18 | except ValueError: 19 | pass 20 | try: 21 | if "hacs" in hass.data.get("frontend_panels", {}): 22 | hacs.log.info("Removing sidepanel") 23 | hass.components.frontend.async_remove_panel("hacs") 24 | except AttributeError: 25 | pass 26 | if DOMAIN in hass.data: 27 | del hass.data[DOMAIN] 28 | hacs.disable(HacsDisabledReason.REMOVED) 29 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/runtime.py: -------------------------------------------------------------------------------- 1 | """Runtime...""" 2 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/setup_actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/custom_components/hacs/operational/setup_actions/__init__.py -------------------------------------------------------------------------------- /custom_components/hacs/operational/setup_actions/categories.py: -------------------------------------------------------------------------------- 1 | """Starting setup task: extra stores.""" 2 | from custom_components.hacs.const import ELEMENT_TYPES 3 | 4 | from ...enums import HacsCategory, HacsSetupTask 5 | from ...share import get_hacs 6 | 7 | 8 | def _setup_extra_stores(): 9 | """Set up extra stores in HACS if enabled in Home Assistant.""" 10 | hacs = get_hacs() 11 | hacs.log.debug("Starting setup task: Extra stores") 12 | hacs.common.categories = set() 13 | for category in ELEMENT_TYPES: 14 | enable_category(hacs, HacsCategory(category)) 15 | 16 | if HacsCategory.PYTHON_SCRIPT in hacs.hass.config.components: 17 | enable_category(hacs, HacsCategory.PYTHON_SCRIPT) 18 | 19 | if ( 20 | hacs.hass.services._services.get("frontend", {}).get("reload_themes") 21 | is not None 22 | ): 23 | enable_category(hacs, HacsCategory.THEME) 24 | 25 | if hacs.configuration.appdaemon: 26 | enable_category(hacs, HacsCategory.APPDAEMON) 27 | if hacs.configuration.netdaemon: 28 | enable_category(hacs, HacsCategory.NETDAEMON) 29 | 30 | 31 | async def async_setup_extra_stores(): 32 | """Async wrapper for setup_extra_stores""" 33 | hacs = get_hacs() 34 | hacs.log.info("setup task %s", HacsSetupTask.CATEGORIES) 35 | await hacs.hass.async_add_executor_job(_setup_extra_stores) 36 | 37 | 38 | def enable_category(hacs, category: HacsCategory): 39 | """Add category.""" 40 | if category not in hacs.common.categories: 41 | hacs.log.info("Enable category: %s", category) 42 | hacs.common.categories.add(category) 43 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/setup_actions/clear_storage.py: -------------------------------------------------------------------------------- 1 | """Starting setup task: clear storage.""" 2 | import os 3 | 4 | from custom_components.hacs.share import get_hacs 5 | 6 | from ...enums import HacsSetupTask 7 | 8 | 9 | async def async_clear_storage(): 10 | """Async wrapper for clear_storage""" 11 | hacs = get_hacs() 12 | hacs.log.info("Setup task %s", HacsSetupTask.CATEGORIES) 13 | await hacs.hass.async_add_executor_job(_clear_storage) 14 | 15 | 16 | def _clear_storage(): 17 | """Clear old files from storage.""" 18 | hacs = get_hacs() 19 | storagefiles = ["hacs"] 20 | for s_f in storagefiles: 21 | path = f"{hacs.core.config_path}/.storage/{s_f}" 22 | if os.path.isfile(path): 23 | hacs.log.info(f"Cleaning up old storage file {path}") 24 | os.remove(path) 25 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/setup_actions/frontend.py: -------------------------------------------------------------------------------- 1 | from hacs_frontend.version import VERSION as FE_VERSION 2 | from hacs_frontend import locate_dir 3 | 4 | from custom_components.hacs.helpers.functions.logger import getLogger 5 | from custom_components.hacs.webresponses.frontend import HacsFrontendDev 6 | from custom_components.hacs.helpers.functions.information import get_frontend_version 7 | from custom_components.hacs.share import get_hacs 8 | 9 | from ...enums import HacsSetupTask 10 | 11 | 12 | URL_BASE = "/hacsfiles" 13 | 14 | 15 | async def async_setup_frontend(): 16 | """Configure the HACS frontend elements.""" 17 | hacs = get_hacs() 18 | hacs.log.info("Setup task %s", HacsSetupTask.FRONTEND) 19 | hass = hacs.hass 20 | 21 | # Register themes 22 | hass.http.register_static_path(f"{URL_BASE}/themes", hass.config.path("themes")) 23 | 24 | # Register frontend 25 | if hacs.configuration.frontend_repo_url: 26 | getLogger().warning( 27 | "Frontend development mode enabled. Do not run in production." 28 | ) 29 | hass.http.register_view(HacsFrontendDev()) 30 | else: 31 | # 32 | hass.http.register_static_path( 33 | f"{URL_BASE}/frontend", locate_dir(), cache_headers=False 34 | ) 35 | 36 | # Custom iconset 37 | hass.http.register_static_path( 38 | f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js") 39 | ) 40 | if "frontend_extra_module_url" not in hass.data: 41 | hass.data["frontend_extra_module_url"] = set() 42 | hass.data["frontend_extra_module_url"].add("/hacsfiles/iconset.js") 43 | 44 | # Register www/community for all other files 45 | hass.http.register_static_path( 46 | URL_BASE, hass.config.path("www/community"), cache_headers=False 47 | ) 48 | 49 | hacs.frontend.version_running = FE_VERSION 50 | hacs.frontend.version_expected = await hass.async_add_executor_job( 51 | get_frontend_version 52 | ) 53 | 54 | # Add to sidepanel 55 | if "hacs" not in hass.data.get("frontend_panels", {}): 56 | hass.components.frontend.async_register_built_in_panel( 57 | component_name="custom", 58 | sidebar_title=hacs.configuration.sidepanel_title, 59 | sidebar_icon=hacs.configuration.sidepanel_icon, 60 | frontend_url_path="hacs", 61 | config={ 62 | "_panel_custom": { 63 | "name": "hacs-frontend", 64 | "embed_iframe": True, 65 | "trust_external": False, 66 | "js_url": "/hacsfiles/frontend/entrypoint.js", 67 | } 68 | }, 69 | require_admin=True, 70 | ) 71 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/setup_actions/load_hacs_repository.py: -------------------------------------------------------------------------------- 1 | """Starting setup task: load HACS repository.""" 2 | from custom_components.hacs.const import INTEGRATION_VERSION 3 | from custom_components.hacs.helpers.classes.exceptions import HacsException 4 | from custom_components.hacs.helpers.functions.information import get_repository 5 | from custom_components.hacs.helpers.functions.register_repository import ( 6 | register_repository, 7 | ) 8 | from custom_components.hacs.share import get_hacs 9 | 10 | from ...enums import HacsSetupTask 11 | 12 | 13 | async def async_load_hacs_repository(): 14 | """Load HACS repositroy.""" 15 | hacs = get_hacs() 16 | hacs.log.info("Setup task %s", HacsSetupTask.HACS_REPO) 17 | 18 | try: 19 | repository = hacs.get_by_name("hacs/integration") 20 | if repository is None: 21 | await register_repository("hacs/integration", "integration") 22 | repository = hacs.get_by_name("hacs/integration") 23 | if repository is None: 24 | raise HacsException("Unknown error") 25 | repository.data.installed = True 26 | repository.data.installed_version = INTEGRATION_VERSION 27 | repository.data.new = False 28 | hacs.repo = repository.repository_object 29 | hacs.data_repo, _ = await get_repository( 30 | hacs.session, hacs.configuration.token, "hacs/default", None 31 | ) 32 | except HacsException as exception: 33 | if "403" in f"{exception}": 34 | hacs.log.critical("GitHub API is ratelimited, or the token is wrong.") 35 | else: 36 | hacs.log.critical(f"[{exception}] - Could not load HACS!") 37 | return False 38 | return True 39 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/setup_actions/sensor.py: -------------------------------------------------------------------------------- 1 | """"Starting setup task: Sensor".""" 2 | from homeassistant.helpers import discovery 3 | 4 | from custom_components.hacs.const import DOMAIN 5 | from custom_components.hacs.share import get_hacs 6 | 7 | from ...enums import HacsSetupTask 8 | 9 | 10 | async def async_add_sensor(): 11 | """Async wrapper for add sensor""" 12 | hacs = get_hacs() 13 | hacs.log.info("Setup task %s", HacsSetupTask.SENSOR) 14 | if hacs.configuration.config_type == "yaml": 15 | hacs.hass.async_create_task( 16 | discovery.async_load_platform( 17 | hacs.hass, "sensor", DOMAIN, {}, hacs.configuration.config 18 | ) 19 | ) 20 | else: 21 | hacs.hass.async_add_job( 22 | hacs.hass.config_entries.async_forward_entry_setup( 23 | hacs.configuration.config_entry, "sensor" 24 | ) 25 | ) 26 | -------------------------------------------------------------------------------- /custom_components/hacs/operational/setup_actions/websocket_api.py: -------------------------------------------------------------------------------- 1 | """Register WS API endpoints for HACS.""" 2 | from homeassistant.components import websocket_api 3 | 4 | from custom_components.hacs.api.acknowledge_critical_repository import ( 5 | acknowledge_critical_repository, 6 | ) 7 | from custom_components.hacs.api.check_local_path import check_local_path 8 | from custom_components.hacs.api.get_critical_repositories import ( 9 | get_critical_repositories, 10 | ) 11 | from custom_components.hacs.api.hacs_config import hacs_config 12 | from custom_components.hacs.api.hacs_removed import hacs_removed 13 | from custom_components.hacs.api.hacs_repositories import hacs_repositories 14 | from custom_components.hacs.api.hacs_repository import hacs_repository 15 | from custom_components.hacs.api.hacs_repository_data import hacs_repository_data 16 | from custom_components.hacs.api.hacs_settings import hacs_settings 17 | from custom_components.hacs.api.hacs_status import hacs_status 18 | from custom_components.hacs.share import get_hacs 19 | 20 | from ...enums import HacsSetupTask 21 | 22 | 23 | async def async_setup_hacs_websockt_api(): 24 | """Set up WS API handlers.""" 25 | hacs = get_hacs() 26 | hacs.log.info("Setup task %s", HacsSetupTask.WEBSOCKET) 27 | websocket_api.async_register_command(hacs.hass, hacs_settings) 28 | websocket_api.async_register_command(hacs.hass, hacs_config) 29 | websocket_api.async_register_command(hacs.hass, hacs_repositories) 30 | websocket_api.async_register_command(hacs.hass, hacs_repository) 31 | websocket_api.async_register_command(hacs.hass, hacs_repository_data) 32 | websocket_api.async_register_command(hacs.hass, check_local_path) 33 | websocket_api.async_register_command(hacs.hass, hacs_status) 34 | websocket_api.async_register_command(hacs.hass, hacs_removed) 35 | websocket_api.async_register_command(hacs.hass, acknowledge_critical_repository) 36 | websocket_api.async_register_command(hacs.hass, get_critical_repositories) 37 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize repositories.""" 2 | from custom_components.hacs.repositories.appdaemon import HacsAppdaemon 3 | from custom_components.hacs.repositories.integration import HacsIntegration 4 | from custom_components.hacs.repositories.netdaemon import HacsNetdaemon 5 | from custom_components.hacs.repositories.plugin import HacsPlugin 6 | from custom_components.hacs.repositories.python_script import HacsPythonScript 7 | from custom_components.hacs.repositories.theme import HacsTheme 8 | 9 | RERPOSITORY_CLASSES = { 10 | "theme": HacsTheme, 11 | "integration": HacsIntegration, 12 | "python_script": HacsPythonScript, 13 | "appdaemon": HacsAppdaemon, 14 | "netdaemon": HacsNetdaemon, 15 | "plugin": HacsPlugin, 16 | } 17 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/appdaemon.py: -------------------------------------------------------------------------------- 1 | """Class for appdaemon apps in HACS.""" 2 | from aiogithubapi import AIOGitHubAPIException 3 | 4 | from custom_components.hacs.enums import HacsCategory 5 | from custom_components.hacs.helpers.classes.exceptions import HacsException 6 | from custom_components.hacs.helpers.classes.repository import HacsRepository 7 | 8 | 9 | class HacsAppdaemon(HacsRepository): 10 | """Appdaemon apps in HACS.""" 11 | 12 | def __init__(self, full_name): 13 | """Initialize.""" 14 | super().__init__() 15 | self.data.full_name = full_name 16 | self.data.full_name_lower = full_name.lower() 17 | self.data.category = HacsCategory.APPDAEMON 18 | self.content.path.local = self.localpath 19 | self.content.path.remote = "apps" 20 | 21 | @property 22 | def localpath(self): 23 | """Return localpath.""" 24 | return f"{self.hacs.core.config_path}/appdaemon/apps/{self.data.name}" 25 | 26 | async def validate_repository(self): 27 | """Validate.""" 28 | await self.common_validate() 29 | 30 | # Custom step 1: Validate content. 31 | try: 32 | addir = await self.repository_object.get_contents("apps", self.ref) 33 | except AIOGitHubAPIException: 34 | raise HacsException( 35 | f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" 36 | ) from None 37 | 38 | if not isinstance(addir, list): 39 | self.validate.errors.append("Repostitory structure not compliant") 40 | 41 | self.content.path.remote = addir[0].path 42 | self.content.objects = await self.repository_object.get_contents( 43 | self.content.path.remote, self.ref 44 | ) 45 | 46 | # Handle potential errors 47 | if self.validate.errors: 48 | for error in self.validate.errors: 49 | if not self.hacs.status.startup: 50 | self.logger.error("%s %s", self, error) 51 | return self.validate.success 52 | 53 | async def update_repository(self, ignore_issues=False, force=False): 54 | """Update.""" 55 | if not await self.common_update(ignore_issues, force): 56 | return 57 | 58 | # Get appdaemon objects. 59 | if self.repository_manifest: 60 | if self.data.content_in_root: 61 | self.content.path.remote = "" 62 | 63 | if self.content.path.remote == "apps": 64 | addir = await self.repository_object.get_contents( 65 | self.content.path.remote, self.ref 66 | ) 67 | self.content.path.remote = addir[0].path 68 | self.content.objects = await self.repository_object.get_contents( 69 | self.content.path.remote, self.ref 70 | ) 71 | 72 | # Set local path 73 | self.content.path.local = self.localpath 74 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/plugin.py: -------------------------------------------------------------------------------- 1 | """Class for plugins in HACS.""" 2 | import json 3 | 4 | from custom_components.hacs.helpers.classes.exceptions import HacsException 5 | from custom_components.hacs.helpers.classes.repository import HacsRepository 6 | from custom_components.hacs.helpers.functions.information import find_file_name 7 | from custom_components.hacs.helpers.functions.logger import getLogger 8 | 9 | 10 | class HacsPlugin(HacsRepository): 11 | """Plugins in HACS.""" 12 | 13 | def __init__(self, full_name): 14 | """Initialize.""" 15 | super().__init__() 16 | self.data.full_name = full_name 17 | self.data.full_name_lower = full_name.lower() 18 | self.data.file_name = None 19 | self.data.category = "plugin" 20 | self.information.javascript_type = None 21 | self.content.path.local = self.localpath 22 | 23 | @property 24 | def localpath(self): 25 | """Return localpath.""" 26 | return f"{self.hacs.core.config_path}/www/community/{self.data.full_name.split('/')[-1]}" 27 | 28 | async def validate_repository(self): 29 | """Validate.""" 30 | # Run common validation steps. 31 | await self.common_validate() 32 | 33 | # Custom step 1: Validate content. 34 | find_file_name(self) 35 | 36 | if self.content.path.remote is None: 37 | raise HacsException( 38 | f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" 39 | ) 40 | 41 | if self.content.path.remote == "release": 42 | self.content.single = True 43 | 44 | # Handle potential errors 45 | if self.validate.errors: 46 | for error in self.validate.errors: 47 | if not self.hacs.status.startup: 48 | self.logger.error("%s %s", self, error) 49 | return self.validate.success 50 | 51 | async def update_repository(self, ignore_issues=False, force=False): 52 | """Update.""" 53 | if not await self.common_update(ignore_issues, force): 54 | return 55 | 56 | # Get plugin objects. 57 | find_file_name(self) 58 | 59 | if self.content.path.remote is None: 60 | self.validate.errors.append( 61 | f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" 62 | ) 63 | 64 | if self.content.path.remote == "release": 65 | self.content.single = True 66 | 67 | async def get_package_content(self): 68 | """Get package content.""" 69 | try: 70 | package = await self.repository_object.get_contents( 71 | "package.json", self.ref 72 | ) 73 | package = json.loads(package.content) 74 | 75 | if package: 76 | self.data.authors = package["author"] 77 | except (Exception, BaseException): # pylint: disable=broad-except 78 | pass 79 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/python_script.py: -------------------------------------------------------------------------------- 1 | """Class for python_scripts in HACS.""" 2 | from custom_components.hacs.enums import HacsCategory 3 | from custom_components.hacs.helpers.classes.exceptions import HacsException 4 | from custom_components.hacs.helpers.classes.repository import HacsRepository 5 | from custom_components.hacs.helpers.functions.information import find_file_name 6 | from custom_components.hacs.helpers.functions.logger import getLogger 7 | 8 | 9 | class HacsPythonScript(HacsRepository): 10 | """python_scripts in HACS.""" 11 | 12 | category = "python_script" 13 | 14 | def __init__(self, full_name): 15 | """Initialize.""" 16 | super().__init__() 17 | self.data.full_name = full_name 18 | self.data.full_name_lower = full_name.lower() 19 | self.data.category = HacsCategory.PYTHON_SCRIPT 20 | self.content.path.remote = "python_scripts" 21 | self.content.path.local = self.localpath 22 | self.content.single = True 23 | 24 | @property 25 | def localpath(self): 26 | """Return localpath.""" 27 | return f"{self.hacs.core.config_path}/python_scripts" 28 | 29 | async def validate_repository(self): 30 | """Validate.""" 31 | # Run common validation steps. 32 | await self.common_validate() 33 | 34 | # Custom step 1: Validate content. 35 | if self.data.content_in_root: 36 | self.content.path.remote = "" 37 | 38 | compliant = False 39 | for treefile in self.treefiles: 40 | if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( 41 | ".py" 42 | ): 43 | compliant = True 44 | break 45 | if not compliant: 46 | raise HacsException( 47 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 48 | ) 49 | 50 | # Handle potential errors 51 | if self.validate.errors: 52 | for error in self.validate.errors: 53 | if not self.hacs.status.startup: 54 | self.logger.error("%s %s", self, error) 55 | return self.validate.success 56 | 57 | async def async_post_registration(self): 58 | """Registration.""" 59 | # Set name 60 | find_file_name(self) 61 | 62 | async def update_repository(self, ignore_issues=False, force=False): 63 | """Update.""" 64 | if not await self.common_update(ignore_issues, force): 65 | return 66 | 67 | # Get python_script objects. 68 | if self.data.content_in_root: 69 | self.content.path.remote = "" 70 | 71 | compliant = False 72 | for treefile in self.treefiles: 73 | if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( 74 | ".py" 75 | ): 76 | compliant = True 77 | break 78 | if not compliant: 79 | raise HacsException( 80 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 81 | ) 82 | 83 | # Update name 84 | find_file_name(self) 85 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/theme.py: -------------------------------------------------------------------------------- 1 | """Class for themes in HACS.""" 2 | from custom_components.hacs.enums import HacsCategory 3 | from custom_components.hacs.helpers.classes.exceptions import HacsException 4 | from custom_components.hacs.helpers.classes.repository import HacsRepository 5 | from custom_components.hacs.helpers.functions.information import find_file_name 6 | from custom_components.hacs.helpers.functions.logger import getLogger 7 | 8 | 9 | class HacsTheme(HacsRepository): 10 | """Themes in HACS.""" 11 | 12 | def __init__(self, full_name): 13 | """Initialize.""" 14 | super().__init__() 15 | self.data.full_name = full_name 16 | self.data.full_name_lower = full_name.lower() 17 | self.data.category = HacsCategory.THEME 18 | self.content.path.remote = "themes" 19 | self.content.path.local = self.localpath 20 | self.content.single = False 21 | 22 | @property 23 | def localpath(self): 24 | """Return localpath.""" 25 | return f"{self.hacs.core.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" 26 | 27 | async def async_post_installation(self): 28 | """Run post installation steps.""" 29 | try: 30 | await self.hacs.hass.services.async_call("frontend", "reload_themes", {}) 31 | except (Exception, BaseException): # pylint: disable=broad-except 32 | pass 33 | 34 | async def validate_repository(self): 35 | """Validate.""" 36 | # Run common validation steps. 37 | await self.common_validate() 38 | 39 | # Custom step 1: Validate content. 40 | compliant = False 41 | for treefile in self.treefiles: 42 | if treefile.startswith("themes/") and treefile.endswith(".yaml"): 43 | compliant = True 44 | break 45 | if not compliant: 46 | raise HacsException( 47 | f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" 48 | ) 49 | 50 | if self.data.content_in_root: 51 | self.content.path.remote = "" 52 | 53 | # Handle potential errors 54 | if self.validate.errors: 55 | for error in self.validate.errors: 56 | if not self.hacs.status.startup: 57 | self.logger.error("%s %s", self, error) 58 | return self.validate.success 59 | 60 | async def async_post_registration(self): 61 | """Registration.""" 62 | # Set name 63 | find_file_name(self) 64 | self.content.path.local = self.localpath 65 | 66 | async def update_repository(self, ignore_issues=False, force=False): 67 | """Update.""" 68 | if not await self.common_update(ignore_issues, force): 69 | return 70 | 71 | # Get theme objects. 72 | if self.data.content_in_root: 73 | self.content.path.remote = "" 74 | 75 | # Update name 76 | find_file_name(self) 77 | self.content.path.local = self.localpath 78 | -------------------------------------------------------------------------------- /custom_components/hacs/share.py: -------------------------------------------------------------------------------- 1 | """Shared HACS elements.""" 2 | import os 3 | 4 | from .base import HacsBase 5 | 6 | SHARE = { 7 | "hacs": None, 8 | "factory": None, 9 | "queue": None, 10 | "removed_repositories": [], 11 | "rules": {}, 12 | } 13 | 14 | 15 | def get_hacs() -> HacsBase: 16 | if SHARE["hacs"] is None: 17 | from custom_components.hacs.hacsbase.hacs import Hacs as Legacy 18 | 19 | _hacs = Legacy() 20 | 21 | if not "PYTEST" in os.environ and "GITHUB_ACTION" in os.environ: 22 | _hacs.system.action = True 23 | 24 | SHARE["hacs"] = _hacs 25 | 26 | return SHARE["hacs"] 27 | 28 | 29 | def get_factory(): 30 | if SHARE["factory"] is None: 31 | from custom_components.hacs.operational.factory import HacsTaskFactory 32 | 33 | SHARE["factory"] = HacsTaskFactory() 34 | 35 | return SHARE["factory"] 36 | 37 | 38 | def get_queue(): 39 | if SHARE["queue"] is None: 40 | from queueman import QueueManager 41 | 42 | SHARE["queue"] = QueueManager() 43 | 44 | return SHARE["queue"] 45 | 46 | 47 | def is_removed(repository): 48 | return repository in [x.repository for x in SHARE["removed_repositories"]] 49 | 50 | 51 | def get_removed(repository): 52 | if not is_removed(repository): 53 | from custom_components.hacs.helpers.classes.removed import RemovedRepository 54 | 55 | removed_repo = RemovedRepository() 56 | removed_repo.repository = repository 57 | SHARE["removed_repositories"].append(removed_repo) 58 | filter_repos = [ 59 | x 60 | for x in SHARE["removed_repositories"] 61 | if x.repository.lower() == repository.lower() 62 | ] 63 | 64 | return filter_repos.pop() or None 65 | 66 | 67 | def list_removed_repositories(): 68 | return SHARE["removed_repositories"] 69 | -------------------------------------------------------------------------------- /custom_components/hacs/system_health.py: -------------------------------------------------------------------------------- 1 | """Provide info to system health.""" 2 | from aiogithubapi.common.const import BASE_API_URL 3 | from homeassistant.components import system_health 4 | from homeassistant.core import HomeAssistant, callback 5 | 6 | from .base import HacsBase 7 | from .const import DOMAIN 8 | 9 | GITHUB_STATUS = "https://www.githubstatus.com/" 10 | 11 | 12 | @callback 13 | def async_register( 14 | hass: HomeAssistant, register: system_health.SystemHealthRegistration 15 | ) -> None: 16 | """Register system health callbacks.""" 17 | register.domain = "Home Assistant Community Store" 18 | register.async_register_info(system_health_info, "/hacs") 19 | 20 | 21 | async def system_health_info(hass): 22 | """Get info for the info page.""" 23 | client: HacsBase = hass.data[DOMAIN] 24 | rate_limit = await client.github.get_rate_limit() 25 | 26 | return { 27 | "GitHub API": system_health.async_check_can_reach_url( 28 | hass, BASE_API_URL, GITHUB_STATUS 29 | ), 30 | "Github API Calls Remaining": rate_limit.get("remaining", "0"), 31 | "Installed Version": client.version, 32 | "Stage": client.stage, 33 | "Available Repositories": len(client.repositories), 34 | "Installed Repositories": len( 35 | [repo for repo in client.repositories if repo.data.installed] 36 | ), 37 | } 38 | -------------------------------------------------------------------------------- /custom_components/hacs/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "single_instance_allowed": "Only a single configuration of HACS is allowed.", 5 | "min_ha_version": "You need at least version {version} of Home Assistant to setup HACS.", 6 | "github": "Could not authenticate with GitHub, try again later.", 7 | "not_setup": "HACS is not setup." 8 | }, 9 | "error": { 10 | "auth": "Personal Access Token is not correct", 11 | "acc": "You need to acknowledge all the statements before continuing" 12 | }, 13 | "step": { 14 | "user": { 15 | "data": { 16 | "acc_logs": "I know how to access Home Assistant logs", 17 | "acc_addons": "I know that there are no add-ons in HACS", 18 | "acc_untested": "I know that everything inside HACS is custom and untested by Home Assistant", 19 | "acc_disable": "I know that if I get issues with Home Assistant I should disable all my custom_components" 20 | }, 21 | "description": "Before you can setup HACS you need to acknowledge the following", 22 | "title": "HACS" 23 | }, 24 | "device": { 25 | "title": "Waiting for device activation" 26 | } 27 | }, 28 | "progress": { 29 | "wait_for_device": "1. Open {url} \n2.Paste the following key to authorize HACS: \n```\n{code}\n```\n" 30 | } 31 | }, 32 | "options": { 33 | "step": { 34 | "user": { 35 | "data": { 36 | "not_in_use": "Not in use with YAML", 37 | "country": "Filter with country code.", 38 | "experimental": "Enable experimental features", 39 | "release_limit": "Number of releases to show.", 40 | "debug": "Enable debug.", 41 | "appdaemon": "Enable AppDaemon apps discovery & tracking", 42 | "netdaemon": "Enable NetDaemon apps discovery & tracking", 43 | "sidepanel_icon": "Side panel icon", 44 | "sidepanel_title": "Side panel title" 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /custom_components/hacs/validate/README.md: -------------------------------------------------------------------------------- 1 | # Repository validation 2 | 3 | This is where the validation rules that run against the various repository categories live. 4 | 5 | ## Structure 6 | 7 | - All validation rules are in the directory for their category. 8 | - Validation rules that aplies to all categories are in the `common` directory. 9 | - There is one file pr. rule. 10 | - All rule needs tests to verify every possible outcome for the rule. 11 | - It's better with multiple files than a big rule. 12 | - All rules uses `ValidationBase` or `ActionValidationBase` as the base class. 13 | - The `ActionValidationBase` are for checks that will breaks compatibility with with existing repositories (default), so these are only run in github actions. 14 | - The class name should describe what the check does. 15 | - Only use `validate` or `async_validate` methods to define validation rules. 16 | - If a rule should fail, raise `ValidationException` with the failure message. 17 | 18 | 19 | ## Example 20 | 21 | ```python 22 | from custom_components.hacs.validate.base import ( 23 | ActionValidationBase, 24 | ValidationBase, 25 | ValidationException, 26 | ) 27 | 28 | 29 | class AwesomeRepository(ValidationBase): 30 | def validate(self): 31 | if self.repository != "awesome": 32 | raise ValidationException("The repository is not awesome") 33 | 34 | class SuperAwesomeRepository(ActionValidationBase, category="integration"): 35 | async def async_validate(self): 36 | if self.repository != "super-awesome": 37 | raise ValidationException("The repository is not super-awesome") 38 | ``` -------------------------------------------------------------------------------- /custom_components/hacs/validate/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import glob 3 | import importlib 4 | from os.path import dirname, join, sep 5 | 6 | from custom_components.hacs.share import SHARE, get_hacs 7 | 8 | 9 | def _initialize_rules(): 10 | rules = glob.glob(join(dirname(__file__), "**/*.py")) 11 | for rule in rules: 12 | rule = rule.replace(sep, "/") 13 | rule = rule.split("custom_components/hacs")[-1] 14 | rule = f"custom_components/hacs{rule}".replace("/", ".")[:-3] 15 | importlib.import_module(rule) 16 | 17 | 18 | async def async_initialize_rules(): 19 | hass = get_hacs().hass 20 | await hass.async_add_executor_job(_initialize_rules) 21 | 22 | 23 | async def async_run_repository_checks(repository): 24 | hacs = get_hacs() 25 | if not SHARE["rules"]: 26 | await async_initialize_rules() 27 | if not hacs.system.running: 28 | return 29 | checks = [] 30 | for check in SHARE["rules"].get("common", []): 31 | checks.append(check(repository)) 32 | for check in SHARE["rules"].get(repository.data.category, []): 33 | checks.append(check(repository)) 34 | 35 | await asyncio.gather( 36 | *[ 37 | check._async_run_check() 38 | for check in checks or [] 39 | if hacs.system.action or not check.action_only 40 | ] 41 | ) 42 | 43 | total = len([x for x in checks if hacs.system.action or not x.action_only]) 44 | failed = len([x for x in checks if x.failed]) 45 | 46 | if failed != 0: 47 | repository.logger.error("%s %s/%s checks failed", repository, failed, total) 48 | if hacs.system.action: 49 | exit(1) 50 | else: 51 | repository.logger.debug("%s All (%s) checks passed", repository, total) 52 | -------------------------------------------------------------------------------- /custom_components/hacs/validate/base.py: -------------------------------------------------------------------------------- 1 | from custom_components.hacs.share import SHARE, get_hacs 2 | 3 | 4 | class ValidationException(Exception): 5 | pass 6 | 7 | 8 | class ValidationBase: 9 | def __init__(self, repository) -> None: 10 | self.repository = repository 11 | self.hacs = get_hacs() 12 | self.failed = False 13 | self.logger = repository.logger 14 | 15 | def __init_subclass__(cls, category="common", **kwargs) -> None: 16 | """Initialize a subclass, register if possible.""" 17 | super().__init_subclass__(**kwargs) 18 | if SHARE["rules"].get(category) is None: 19 | SHARE["rules"][category] = [] 20 | if cls not in SHARE["rules"][category]: 21 | SHARE["rules"][category].append(cls) 22 | 23 | @property 24 | def action_only(self): 25 | return False 26 | 27 | async def _async_run_check(self): 28 | """DO NOT OVERRIDE THIS IN SUBCLASSES!""" 29 | if self.hacs.system.action: 30 | self.logger.info(f"Running check '{self.__class__.__name__}'") 31 | try: 32 | await self.hacs.hass.async_add_executor_job(self.check) 33 | await self.async_check() 34 | except ValidationException as exception: 35 | self.failed = True 36 | self.logger.error(exception) 37 | 38 | def check(self): 39 | pass 40 | 41 | async def async_check(self): 42 | pass 43 | 44 | 45 | class ActionValidationBase(ValidationBase): 46 | @property 47 | def action_only(self): 48 | return True 49 | -------------------------------------------------------------------------------- /custom_components/hacs/validate/common/hacs_manifest.py: -------------------------------------------------------------------------------- 1 | from custom_components.hacs.validate.base import ( 2 | ActionValidationBase, 3 | ValidationException, 4 | ) 5 | 6 | 7 | class HacsManifest(ActionValidationBase): 8 | def check(self): 9 | if "hacs.json" not in [x.filename for x in self.repository.tree]: 10 | raise ValidationException("The repository has no 'hacs.json' file") 11 | -------------------------------------------------------------------------------- /custom_components/hacs/validate/common/repository_description.py: -------------------------------------------------------------------------------- 1 | from custom_components.hacs.validate.base import ( 2 | ActionValidationBase, 3 | ValidationException, 4 | ) 5 | 6 | 7 | class RepositoryDescription(ActionValidationBase): 8 | def check(self): 9 | if not self.repository.data.description: 10 | raise ValidationException("The repository has no description") 11 | -------------------------------------------------------------------------------- /custom_components/hacs/validate/common/repository_information_file.py: -------------------------------------------------------------------------------- 1 | from custom_components.hacs.validate.base import ( 2 | ActionValidationBase, 3 | ValidationException, 4 | ) 5 | 6 | 7 | class RepositoryInformationFile(ActionValidationBase): 8 | async def async_check(self): 9 | filenames = [x.filename.lower() for x in self.repository.tree] 10 | if self.repository.data.render_readme and "readme" in filenames: 11 | pass 12 | elif self.repository.data.render_readme and "readme.md" in filenames: 13 | pass 14 | elif "info" in filenames: 15 | pass 16 | elif "info.md" in filenames: 17 | pass 18 | else: 19 | raise ValidationException("The repository has no information file") 20 | -------------------------------------------------------------------------------- /custom_components/hacs/validate/common/repository_topics.py: -------------------------------------------------------------------------------- 1 | from custom_components.hacs.validate.base import ( 2 | ActionValidationBase, 3 | ValidationException, 4 | ) 5 | 6 | 7 | class RepositoryTopics(ActionValidationBase): 8 | def check(self): 9 | if not self.repository.data.topics: 10 | raise ValidationException("The repository has no topics") 11 | -------------------------------------------------------------------------------- /custom_components/hacs/validate/integration/integration_manifest.py: -------------------------------------------------------------------------------- 1 | from custom_components.hacs.validate.base import ( 2 | ActionValidationBase, 3 | ValidationException, 4 | ) 5 | 6 | 7 | class IntegrationManifest(ActionValidationBase, category="integration"): 8 | def check(self): 9 | if "manifest.json" not in [x.filename for x in self.repository.tree]: 10 | raise ValidationException("The repository has no 'hacs.json' file") 11 | -------------------------------------------------------------------------------- /custom_components/hacs/webresponses/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize HACS Web responses""" 2 | -------------------------------------------------------------------------------- /custom_components/hacs/webresponses/frontend.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | from homeassistant.components.http import HomeAssistantView 4 | from custom_components.hacs.share import get_hacs 5 | 6 | 7 | class HacsFrontendDev(HomeAssistantView): 8 | """Dev View Class for HACS.""" 9 | 10 | requires_auth = False 11 | name = "hacs_files:frontend" 12 | url = r"/hacsfiles/frontend/{requested_file:.+}" 13 | 14 | async def get(self, request, requested_file): # pylint: disable=unused-argument 15 | """Handle HACS Web requests.""" 16 | hacs = get_hacs() 17 | requested = requested_file.split("/")[-1] 18 | request = await hacs.session.get( 19 | f"{hacs.configuration.frontend_repo_url}/{requested}" 20 | ) 21 | if request.status == 200: 22 | result = await request.read() 23 | response = web.Response(body=result) 24 | response.headers["Content-Type"] = "application/javascript" 25 | 26 | return response 27 | -------------------------------------------------------------------------------- /custom_components/iaquk/const.py: -------------------------------------------------------------------------------- 1 | """Constants for calculate IAQ UK index.""" 2 | 3 | from homeassistant.components.sensor import DOMAIN as SENSOR 4 | 5 | # Base component constants 6 | NAME = "Indoor Air Quality UK Index" 7 | DOMAIN = "iaquk" 8 | VERSION = "1.4.0" 9 | ISSUE_URL = "https://github.com/Limych/ha-iaquk/issues" 10 | 11 | STARTUP_MESSAGE = f""" 12 | ------------------------------------------------------------------- 13 | {NAME} 14 | Version: {VERSION} 15 | This is a custom integration! 16 | If you have ANY issues with this you need to open an issue here: 17 | {ISSUE_URL} 18 | ------------------------------------------------------------------- 19 | """ 20 | 21 | # Icons 22 | ICON_DEFAULT = "mdi:air-filter" 23 | ICON_EXCELLENT = "mdi:emoticon-excited" 24 | ICON_GOOD = "mdi:emoticon-happy" 25 | ICON_FAIR = "mdi:emoticon-neutral" 26 | ICON_POOR = "mdi:emoticon-sad" 27 | ICON_INADEQUATE = "mdi:emoticon-dead" 28 | 29 | # Device classes 30 | BINARY_SENSOR_DEVICE_CLASS = "connectivity" 31 | 32 | # Platforms 33 | PLATFORMS = [SENSOR] 34 | 35 | # Configuration and options 36 | CONF_SOURCES = "sources" 37 | CONF_TEMPERATURE = "temperature" 38 | CONF_HUMIDITY = "humidity" 39 | CONF_CO2 = "co2" 40 | CONF_TVOC = "tvoc" 41 | CONF_PM = "pm" 42 | CONF_NO2 = "no2" 43 | CONF_CO = "co" 44 | CONF_HCHO = "hcho" # Formaldehyde 45 | CONF_RADON = "radon" 46 | 47 | # Defaults 48 | 49 | # Attributes 50 | ATTR_SOURCES_SET = "sources_set" 51 | ATTR_SOURCES_USED = "sources_used" 52 | ATTR_SOURCE_INDEX_TPL = "{}_index" 53 | 54 | 55 | LEVEL_EXCELLENT = "Excellent" 56 | LEVEL_GOOD = "Good" 57 | LEVEL_FAIR = "Fair" 58 | LEVEL_POOR = "Poor" 59 | LEVEL_INADEQUATE = "Inadequate" 60 | 61 | UNIT_PPM = { 62 | "ppm": 1, # Target unit -- conversion rate will be ignored 63 | "ppb": 0.001, 64 | } 65 | UNIT_PPB = { 66 | "ppb": 1, # Target unit -- conversion rate will be ignored 67 | "ppm": 1000, 68 | } 69 | UNIT_UGM3 = { 70 | "µg/m³": 1, # Target unit -- conversion rate will be ignored 71 | "µg/m3": 1, 72 | "µg/m^3": 1, 73 | "mg/m³": 1000, 74 | "mg/m3": 1000, 75 | "mg/m^3": 1000, 76 | } 77 | UNIT_MGM3 = { 78 | "mg/m³": 1, # Target unit -- conversion rate will be ignored 79 | "mg/m3": 1, 80 | "mg/m^3": 1, 81 | "µg/m³": 0.001, 82 | "µg/m3": 0.001, 83 | "µg/m^3": 0.001, 84 | } 85 | 86 | MWEIGTH_TVOC = 100 # g/mol 87 | MWEIGTH_HCHO = 30.0260 # g/mol 88 | MWEIGTH_CO = 28.0100 # g/mol 89 | MWEIGTH_NO2 = 46.0100 # g/mol 90 | MWEIGTH_CO2 = 44.0100 # g/mol 91 | -------------------------------------------------------------------------------- /custom_components/iaquk/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "iaquk", 3 | "name": "Indoor Air Quality UK Index", 4 | "version": "1.4.0", 5 | "documentation": "https://github.com/Limych/ha-iaquk", 6 | "issue_tracker": "https://github.com/Limych/ha-iaquk/issues", 7 | "dependencies": [], 8 | "config_flow": false, 9 | "codeowners": [ 10 | "@Limych" 11 | ], 12 | "requirements": [] 13 | } 14 | -------------------------------------------------------------------------------- /custom_components/iaquk/translations/sensor.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "iaquk__level": { 4 | "Excellent": "Excellent", 5 | "Good": "Good", 6 | "Fair": "Fair", 7 | "Poor": "Poor", 8 | "Inadequate": "Inadequate" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/iaquk/translations/sensor.ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "iaquk__level": { 4 | "Excellent": "Отличный", 5 | "Good": "Хороший", 6 | "Fair": "Удовлетворительный", 7 | "Poor": "Плохой", 8 | "Inadequate": "Неадекватный" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/jq300/entity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021, Andrey "Limych" Khrolenok 2 | # Creative Commons BY-NC-SA 4.0 International Public License 3 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 4 | 5 | """ 6 | Integration of the JQ-300/200/100 indoor air quality meter. 7 | 8 | For more details about this component, please refer to 9 | https://github.com/Limych/ha-jq300 10 | """ 11 | 12 | import logging 13 | 14 | from homeassistant.const import ATTR_ATTRIBUTION 15 | from homeassistant.exceptions import PlatformNotReady 16 | from homeassistant.helpers.entity import Entity 17 | 18 | from .api import Jq300Account 19 | from .const import ATTRIBUTION, DOMAIN 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | class Jq300Entity(Entity): 25 | """Jq300 entity.""" 26 | 27 | def __init__( 28 | self, entity_id: str, account: Jq300Account, device_id, sensor_id, sensor_state 29 | ): 30 | """Initialize a JQ entity.""" 31 | super().__init__() 32 | 33 | self.entity_id = entity_id 34 | 35 | if account.devices == {}: 36 | raise PlatformNotReady 37 | 38 | self._name = None 39 | self._icon = None 40 | self._account = account 41 | self._device = account.devices.get(device_id, {}) 42 | self._device_id = device_id 43 | self._unique_id = "{}-{}-{}".format( 44 | self._account.unique_id, device_id, sensor_id 45 | ) 46 | self._sensor_id = sensor_id 47 | self._state = sensor_state 48 | 49 | self._device_class = None 50 | 51 | @property 52 | def unique_id(self): 53 | """Return a unique ID to use for this entity.""" 54 | return self._unique_id 55 | 56 | @property 57 | def name(self): 58 | """Return the name of the binary sensor.""" 59 | return self._name 60 | 61 | @property 62 | def icon(self): 63 | """Icon to use in the frontend, if any.""" 64 | return self._icon 65 | 66 | @property 67 | def available(self) -> bool: 68 | """Return True if entity is available.""" 69 | return self._account.device_available(self._device_id) 70 | 71 | @property 72 | def should_poll(self) -> bool: 73 | """Return the polling state.""" 74 | return True 75 | 76 | @property 77 | def device_class(self): 78 | """Return the class of the sensor.""" 79 | return self._device_class 80 | 81 | @property 82 | def device_info(self): 83 | """Return the device info.""" 84 | return { 85 | "identifiers": {(DOMAIN, self.unique_id)}, 86 | "name": self._name, 87 | "manufacturer": self._device.get("brandname"), 88 | "model": self._device.get("pt_model"), 89 | } 90 | 91 | @property 92 | def device_state_attributes(self): 93 | """Return the state attributes.""" 94 | return { 95 | ATTR_ATTRIBUTION: ATTRIBUTION, 96 | } 97 | -------------------------------------------------------------------------------- /custom_components/jq300/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "jq300", 3 | "name": "JQ-300/200/100 Indoor Air Quality Meter", 4 | "version": "0.8.2", 5 | "documentation": "https://github.com/Limych/ha-jq300", 6 | "issue_tracker": "https://github.com/Limych/ha-jq300/issues", 7 | "dependencies": [], 8 | "config_flow": false, 9 | "codeowners": [ 10 | "@Limych" 11 | ], 12 | "requirements": [ 13 | "paho-mqtt==1.5.1" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /custom_components/jq300/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Jq300", 6 | "description": "If you need help with the configuration have a look here: https://github.com/Limych/ha-jq300", 7 | "data": { 8 | "username": "Username", 9 | "password": "Password" 10 | } 11 | } 12 | }, 13 | "error": { 14 | "auth": "Username/Password is wrong." 15 | }, 16 | "abort": { 17 | "single_instance_allowed": "Only a single instance is allowed." 18 | } 19 | }, 20 | "options": { 21 | "step": { 22 | "user": { 23 | "data": { 24 | "binary_sensor": "Binary sensor enabled", 25 | "sensor": "Sensor enabled", 26 | "switch": "Switch enabled" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /custom_components/jq300/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Jq300", 6 | "description": "Если вам нужна помощь с настройкой, посмотрите здесь: https://github.com/Limych/ha-jq300", 7 | "data": { 8 | "username": "Имя пользователя", 9 | "password": "Пароль" 10 | } 11 | } 12 | }, 13 | "error": { 14 | "auth": "Имя пользователя или пароль — неверны." 15 | }, 16 | "abort": { 17 | "single_instance_allowed": "Допускается только один экземпляр." 18 | } 19 | }, 20 | "options": { 21 | "step": { 22 | "user": { 23 | "data": { 24 | "binary_sensor": "Двоичный датчик активен", 25 | "sensor": "Датчик активен", 26 | "switch": "Выключатель активен" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /custom_components/jq300/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021, Andrey "Limych" Khrolenok 2 | # Creative Commons BY-NC-SA 4.0 International Public License 3 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 4 | 5 | """ 6 | Integration of the JQ-300/200/100 indoor air quality meter. 7 | 8 | For more details about this component, please refer to 9 | https://github.com/Limych/ha-jq300 10 | """ 11 | 12 | import logging 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | def mask(text: str, first: int = 2, last: int = 1): 18 | """Mask text by asterisks.""" 19 | tlen = len(text) 20 | to_show = first + last 21 | return ( 22 | ("" if tlen <= to_show else text[:first]) 23 | + "*" * (tlen - (0 if tlen <= to_show else to_show)) 24 | + ("" if tlen <= to_show else text[-last:]) 25 | ) 26 | 27 | 28 | def mask_email(email: str): 29 | """Mask email by asterisks.""" 30 | local, _, domain = email.partition("@") 31 | parts = domain.split(".") 32 | dname = ".".join(parts[:-1]) 33 | dtype = parts[-1] 34 | return "{}@{}.{}".format(mask(local), mask(dname), dtype) 35 | -------------------------------------------------------------------------------- /custom_components/linkplay/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for LinkPlay.""" 2 | import upnpclient 3 | import netdisco.ssdp 4 | from homeassistant import config_entries 5 | from homeassistant.helpers import config_entry_flow 6 | 7 | from . import DOMAIN 8 | 9 | async def _async_has_devices(hass): 10 | """Return if there are devices that can be discovered.""" 11 | return await hass.async_add_executor_job(upnp_discover) 12 | 13 | 14 | config_entry_flow.register_discovery_flow( 15 | DOMAIN, "LinkPlay", _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH 16 | ) 17 | 18 | def upnp_discover(timeout=5): 19 | devices = {} 20 | for entry in netdisco.ssdp.scan(timeout): 21 | if entry.location in devices: 22 | continue 23 | try: 24 | devices[entry.location] = upnpclient.Device(entry.location) 25 | except Exception as exc: 26 | pass 27 | return list(devices.values()) 28 | 29 | -------------------------------------------------------------------------------- /custom_components/linkplay/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "linkplay", 3 | "name": "Linkplay", 4 | "version":"2.0.32", 5 | "documentation": "https://github.com/nagyrobi/home-assistant-custom-components-linkplay", 6 | "issue_tracker": "https://github.com/nagyrobi/home-assistant-custom-components-linkplay/issues", 7 | "after_dependencies": ["http", "media_source", "tts", "ssdp"], 8 | "config_flow": false, 9 | "codeowners": [ 10 | "@nicjo814", 11 | "@limych", 12 | "@nagyrobi" 13 | ], 14 | "requirements": [ 15 | "uPnPClient~=0.0", 16 | "validators~=0.12" 17 | ], 18 | "ssdp": [ 19 | { 20 | "st": "upnp:rootdevice", 21 | "manufacturer": "wiimu", 22 | "device_type": "urn:schemas-upnp-org:device:MediaRenderer:1" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /custom_components/narodmon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "narodmon", 3 | "name": "Narodmon.ru Cloud Integration", 4 | "version": "0.0.0+dev", 5 | "documentation": "https://github.com/Limych/ha-narodmon", 6 | "issue_tracker": "https://github.com/Limych/ha-narodmon/issues", 7 | "dependencies": [], 8 | "config_flow": true, 9 | "codeowners": [ 10 | "@Limych" 11 | ], 12 | "requirements": [] 13 | } 14 | -------------------------------------------------------------------------------- /custom_components/narodmon/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Narodmon.ru Cloud Integration", 6 | "description": "If you need help with the configuration have a look here: https://github.com/Limych/ha-narodmon", 7 | "data": { 8 | "apikey": "API key" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "auth": "API key is wrong." 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Only a single instance is allowed." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /custom_components/narodmon/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Интеграция облака Narodmon.ru", 6 | "description": "Если вам нужна помощь с настройкой, посмотрите здесь: https://github.com/Limych/ha-narodmon", 7 | "data": { 8 | "apikey": "Ключ API" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "auth": "Неверный ключ API." 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Допускается только один экземпляр." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /custom_components/pfsense_gateways/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for pfsense gateways status monitoring using alexpmorris / pfsense-status-gateways-json script 3 | 4 | Copyright (c) 2020 Horvath A. Robert 5 | 6 | Licensed under MIT. All rights reserved. 7 | 8 | https://github.com/nagyrobi/home-assistant-custom-components-pfsense-gateways 9 | """ 10 | -------------------------------------------------------------------------------- /custom_components/pfsense_gateways/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "pfsense_gateways", 3 | "name": "pfSense Gateways", 4 | "version": "v1.01", 5 | "documentation": "https://github.com/nagyrobi/home-assistant-custom-components-pfsense-gateways", 6 | "issue_tracker": "https://github.com/nagyrobi/home-assistant-custom-components-pfsense-gateways/issues", 7 | "dependencies": [], 8 | "codeowners": ["@nagyrobi"], 9 | "requirements": [] 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/shelly/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "shelly", 3 | "name": "Shelly smart home", 4 | "version": "0.2.2", 5 | "config_flow": true, 6 | "documentation": "https://github.com/StyraHem/ShellyForHASS/blob/master/README.md", 7 | "dependencies": ["zeroconf"], 8 | "codeowners": ["@hakana","@StyraHem"], 9 | "requirements": ["pyShelly==0.2.23", "paho-mqtt==1.5.1"] 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/shelly/services.yaml: -------------------------------------------------------------------------------- 1 | # Service ID 2 | set_value: 3 | name: Set value 4 | description: Set specific values for Shelly device 5 | target: 6 | fields: 7 | state: 8 | name: State 9 | description: Set the device state 10 | required: false 11 | example: "true" 12 | default: "true" 13 | selector: 14 | select: 15 | options: 16 | - "true" 17 | - "false" 18 | brightness: 19 | name: Brighness 20 | description: Set brightness of the device (1-255) 21 | required: false 22 | example: "55" 23 | default: "255" 24 | color_temo: 25 | name: Color temperatur 26 | description: Set color temperature 27 | required: false 28 | example: "3000" 29 | default: "3000" 30 | -------------------------------------------------------------------------------- /custom_components/snowtire/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020, Andrey "Limych" Khrolenok 3 | # Creative Commons BY-NC-SA 4.0 International Public License 4 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 5 | # 6 | """ 7 | The Snowtire binary sensor. 8 | 9 | For more details about this platform, please refer to the documentation at 10 | https://github.com/Limych/ha-snowtire/ 11 | """ 12 | -------------------------------------------------------------------------------- /custom_components/snowtire/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Snowtire binary sensor. 3 | 4 | For more details about this platform, please refer to the documentation at 5 | https://github.com/Limych/ha-snowtire/ 6 | """ 7 | 8 | # Base component constants 9 | NAME = "Snowtire Sensor" 10 | DOMAIN = "snowtire" 11 | VERSION = "1.2.1" 12 | ISSUE_URL = "https://github.com/Limych/ha-snowtire/issues" 13 | 14 | STARTUP_MESSAGE = f""" 15 | ------------------------------------------------------------------- 16 | {NAME} 17 | Version: {VERSION} 18 | This is a custom integration! 19 | If you have ANY issues with this you need to open an issue here: 20 | {ISSUE_URL} 21 | ------------------------------------------------------------------- 22 | """ 23 | 24 | # Icons 25 | ICON = "mdi:snowflake" 26 | 27 | # Configuration and options 28 | CONF_WEATHER = "weather" 29 | CONF_DAYS = "days" 30 | 31 | # Defaults 32 | DEFAULT_NAME = "Snowtire" 33 | DEFAULT_DAYS = 7 34 | -------------------------------------------------------------------------------- /custom_components/snowtire/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "snowtire", 3 | "name": "Snowtire Sensor", 4 | "version": "1.2.1", 5 | "documentation": "https://github.com/Limych/ha-snowtire", 6 | "issue_tracker": "https://github.com/Limych/ha-snowtire/issues", 7 | "dependencies": [ 8 | "weather" 9 | ], 10 | "config_flow": false, 11 | "codeowners": [ 12 | "@limych" 13 | ], 14 | "requirements": [] 15 | } 16 | -------------------------------------------------------------------------------- /custom_components/snowtire/translations/binary_sensor.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "snowtire__type": { 4 | "off": "Summer", 5 | "on": "Winter" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/snowtire/translations/binary_sensor.hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "snowtire__type": { 4 | "off": "Nyári", 5 | "on": "Téli" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/snowtire/translations/binary_sensor.it.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "snowtire__type": { 4 | "off": "Estive", 5 | "on": "Invernali" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/snowtire/translations/binary_sensor.pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "snowtire__type": { 4 | "off": "Letnie", 5 | "on": "Zimowe" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/snowtire/translations/binary_sensor.ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "snowtire__type": { 4 | "off": "Летняя", 5 | "on": "Зимняя" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/snowtire/translations/binary_sensor.uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "snowtire__type": { 4 | "off": "Літні", 5 | "on": "Зимові" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/images/desktop_automations.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/desktop_automations.jpg -------------------------------------------------------------------------------- /docs/images/desktop_home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/desktop_home.jpg -------------------------------------------------------------------------------- /docs/images/desktop_home_info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/desktop_home_info.jpg -------------------------------------------------------------------------------- /docs/images/desktop_system_info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/desktop_system_info.jpg -------------------------------------------------------------------------------- /docs/images/mobile_automations.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/mobile_automations.jpg -------------------------------------------------------------------------------- /docs/images/mobile_home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/mobile_home.jpg -------------------------------------------------------------------------------- /docs/images/mobile_home_info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/mobile_home_info.jpg -------------------------------------------------------------------------------- /docs/images/mobile_system_info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/docs/images/mobile_system_info.jpg -------------------------------------------------------------------------------- /esphome/.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | **/.pioenvs/ 6 | **/.piolibdeps/ 7 | **/lib/ 8 | **/src/ 9 | **/platformio.ini 10 | /secrets.yaml 11 | -------------------------------------------------------------------------------- /esphome/README.md: -------------------------------------------------------------------------------- 1 | # ESPHome firmwares 2 | 3 | This directory are store my custom firmwares for some devices. 4 | 5 | ## Bathroom 6 | 7 | Bathroom sensor and exhaust fan controller on Sonoff TH10 + SI7021 temperature & humidity sensor. 8 | 9 | Features: 10 | - Can work autonomously; 11 | - If connected to Home Assistant, automatically once a minute publish current temperature and humidity. 12 | - Automatically turning on fan then humidity above setted threshold and turning off it otherwise; 13 | - You can manually turn on fan for 5 minutes pressing button on sensor body. Each button press reset switch off timer to 5 minutes; 14 | - Via API (through service in Home Assistant) you can set non default humidity threshold permanently or for some time; 15 | - Via API (through service in Home Assistant) you can manually control fan at any time. You can turn on fan permanently or for some time; 16 | 17 | ## Kitchen 18 | 19 | Kitchen sensor and cooling fan controller on Sonoff TH10 + SI7021 temperature & humidity sensor. 20 | 21 | Features: 22 | - Can work autonomously; 23 | - If connected to Home Assistant, automatically once a minute publish current temperature and humidity. 24 | - You can manually turn on fan for 5 minutes pressing button on sensor body. If fun already turning on, button pressing turn it off; 25 | - Via API (through service in Home Assistant) you can manually control fan at any time. You can turn on fan permanently or for some time; 26 | 27 | ## Kitchen_old 28 | 29 | Old version of kitchen sensor. 30 | -------------------------------------------------------------------------------- /external_addons/README.md: -------------------------------------------------------------------------------- 1 | # Add-ons for third-party programs to work with Home Assistant 2 | 3 | ## ha_host_monitor.py 4 | 5 | Module that send to HA main statistic details about host: CPU, memory & drives statistic. -------------------------------------------------------------------------------- /logging/history.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | domains: 3 | - light 4 | - device_tracker 5 | - binary_sensor 6 | - group 7 | - media_player 8 | - person 9 | - proximity 10 | entities: 11 | - input_select.day_mode 12 | - sensor.owm_temperature 13 | - sensor.owm_humidity 14 | - sensor.owm_pressure 15 | - sensor.owm_wind_speed 16 | - sensor.gismeteo_temperature 17 | - sensor.gismeteo_humidity 18 | - sensor.gismeteo_pressure 19 | - sensor.gismeteo_wind_speed 20 | - sensor.pressure 21 | - sensor.kitchen_temperature 22 | - sensor.kitchen_humidity 23 | - sensor.kitchen_co2 24 | - sensor.kitchen_tvoc 25 | - sensor.bathroom_temperature 26 | - sensor.bathroom_humidity 27 | exclude: 28 | domains: 29 | - automation 30 | - alert 31 | - camera 32 | - sun 33 | - calendar 34 | # - sensor 35 | - switch 36 | - script 37 | entities: 38 | - group.all_automations 39 | - group.all_scripts 40 | - group.all_switches 41 | - group.all_lights 42 | - group.all_devices 43 | - sensor.bathroom_drying_ontime 44 | -------------------------------------------------------------------------------- /logging/logbook.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | domains: 3 | - light 4 | - automation 5 | - binary_sensor 6 | - device_tracker 7 | # entities: 8 | exclude: 9 | domains: 10 | - alert 11 | - camera 12 | - media_player 13 | - sun 14 | - calendar 15 | - sensor 16 | - group 17 | - switch 18 | - script 19 | entities: 20 | - sensor.bathroom_drying_ontime 21 | -------------------------------------------------------------------------------- /logging/logger.yaml: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # 3 | # Logger You can filter anything betwen the []'s in the logger. 4 | # 5 | ############################################################################ 6 | 7 | # Log Severities: notset, debug, info, warn, warning, error, fatal, critical 8 | 9 | default: warn 10 | # default: debug 11 | logs: 12 | root: debug 13 | 14 | # beward: debug 15 | 16 | # custom_components.average: debug 17 | # custom_components.beward: debug 18 | # custom_components.car_tire: debug 19 | # custom_components.car_winter_tires: debug 20 | # custom_components.iaquk: debug 21 | # custom_components.jq300: debug 22 | # custom_components.gismeteo: debug 23 | # custom_components.linkplay: debug 24 | # custom_components.narodmon: debug 25 | 26 | # homeassistant.components.api: error 27 | # homeassistant.components.automation: error 28 | # homeassistant.components.binary_sensor: debug 29 | # homeassistant.components.camera: debug 30 | homeassistant.components.device_tracker: warn 31 | # homeassistant.components.device_tracker.owntracks: error 32 | homeassistant.components.discovery: warn 33 | # homeassistant.components.ffmpeg: debug 34 | homeassistant.components.hassio: debug 35 | # homeassistant.components.influxdb: debug 36 | # homeassistant.components.http: error 37 | # homeassistant.components.light: warn 38 | # homeassistant.components.media_player: debug 39 | # homeassistant.components.mqtt: debug 40 | # homeassistant.components.notify: error 41 | # homeassistant.components.python_script: debug 42 | homeassistant.components.sensor: fatal 43 | homeassistant.components.sensor.travisci: debug 44 | homeassistant.components.shell_command: debug 45 | # homeassistant.components.ssdp: debug 46 | # homeassistant.components.telegram: error 47 | # homeassistant.components.utility_meter: debug 48 | # homeassistant.components.websocket_api: error 49 | # homeassistant.components.weather: debug 50 | # homeassistant.components.zeroconf: debug 51 | # homeassistant.core: debug 52 | # homeassistant.helpers.entity: error 53 | homeassistant.helpers.script: debug 54 | -------------------------------------------------------------------------------- /lovelace/30_lights_view.yaml: -------------------------------------------------------------------------------- 1 | title: "Lights" 2 | icon: mdi:lightbulb 3 | panel: true 4 | path: lights 5 | cards: 6 | - type: custom:layout-card 7 | layout_type: custom:vertical-layout 8 | layout: 9 | max_cols: 3 10 | cards: 11 | - type: vertical-stack 12 | cards: 13 | - type: custom:auto-entities 14 | show_empty: false 15 | unique: true 16 | card: 17 | type: entities 18 | title: "Статус зон" 19 | sort: 20 | method: name 21 | ignore_case: true 22 | filter: 23 | include: 24 | - entity_id: "binary_sensor.*_motion" 25 | options: 26 | type: custom:multiple-entity-row 27 | #entity: this.entity_id 28 | show_state: false 29 | state_color: true 30 | secondary_info: last-changed 31 | entities: 32 | - entity: this.entity_id 33 | name: false 34 | - attribute: illuminance 35 | name: "Освещ." 36 | unit: "lx" 37 | hide_unavailable: true 38 | - attribute: battery 39 | name: "Заряд" 40 | unit: "%" 41 | hide_unavailable: true 42 | exclude: 43 | - entity_id: "*_1_*" 44 | - entity_id: "*_2_*" 45 | - entity_id: "binary_sensor.front_door_motion" 46 | - entity_id: "binary_sensor.wallpanel_*" 47 | 48 | - type: custom:layout-break 49 | - type: custom:auto-entities 50 | show_empty: false 51 | unique: true 52 | card: 53 | type: entities 54 | title: "Светильники" 55 | show_header_toggle: true 56 | filter: 57 | include: 58 | - domain: light 59 | options: 60 | secondary_info: brightness 61 | sort: name 62 | 63 | - type: custom:layout-break 64 | - type: custom:auto-entities 65 | show_empty: false 66 | unique: true 67 | card: 68 | type: history-graph 69 | title: "Освещённость за 48ч" 70 | hours_to_show: 48 71 | sort: name 72 | filter: 73 | include: 74 | - entity_id: "sensor.*_illuminance" 75 | exclude: 76 | - entity_id: "*_1_*" 77 | - entity_id: "*_2_*" 78 | - entity_id: "*.narodmon_*" 79 | - type: custom:auto-entities 80 | show_empty: false 81 | unique: true 82 | card: 83 | type: history-graph 84 | title: "Освещённость за 7 дней" 85 | hours_to_show: 168 86 | sort: name 87 | filter: 88 | include: 89 | - entity_id: "sensor.*_illuminance" 90 | exclude: 91 | - entity_id: "*_1_*" 92 | - entity_id: "*_2_*" 93 | - entity_id: "*.narodmon_*" 94 | 95 | - !include cards/dev.yaml 96 | 97 | -------------------------------------------------------------------------------- /lovelace/40_system_info_view.yaml: -------------------------------------------------------------------------------- 1 | title: Info 2 | icon: mdi:lan 3 | path: info 4 | panel: true 5 | cards: 6 | - type: custom:layout-card 7 | layout_type: custom:horizontal-layout 8 | layout: 9 | max_cols: 3 10 | cards: 11 | - type: vertical-stack 12 | cards: 13 | - type: custom:auto-entities 14 | show_empty: false 15 | unique: true 16 | card: 17 | type: glance 18 | title: "Устройства" 19 | show_state: false 20 | columns: 4 21 | filter: 22 | include: 23 | - entity_id: sensor.system_gateway 24 | - entity_id: sensor.system_nas 25 | - entity_id: 'sensor.system_security_*' 26 | - entity_id: 'sensor.system_server_*' 27 | - entity_id: 'sensor.system_*' 28 | - entity_id: 'sensor.*_pc_*' 29 | exclude: 30 | - entity_id: sensor.system_uplink 31 | 32 | - type: custom:auto-entities 33 | show_empty: false 34 | unique: true 35 | card: 36 | type: glance 37 | title: "Соединения" 38 | show_state: false 39 | columns: 3 40 | filter: 41 | include: 42 | - entity_id: sensor.connection_internet 43 | - entity_id: sensor.connection_wifi_ap 44 | - entity_id: 'sensor.connection_*' 45 | 46 | - !include system/nas_monitor_card.yaml 47 | - !include system/hass_monitor_card.yaml 48 | 49 | - type: vertical-stack 50 | cards: 51 | - type: custom:mini-graph-card 52 | name: "Скорость Интернета" 53 | height: 75 54 | hour24: true 55 | show: 56 | icon: false 57 | extrema: true 58 | fill: false 59 | entities: 60 | - entity: sensor.speedtest_download 61 | name: "Приём" 62 | - entity: sensor.speedtest_upload 63 | name: "Отправка" 64 | 65 | - !include cards/dev.yaml 66 | 67 | -------------------------------------------------------------------------------- /lovelace/50_security_view.yaml: -------------------------------------------------------------------------------- 1 | title: "Security" 2 | icon: mdi:shield-home 3 | path: security 4 | panel: true 5 | cards: 6 | - type: custom:layout-card 7 | layout_type: custom:horizontal-layout 8 | layout: 9 | max_cols: 3 10 | cards: 11 | - type: vertical-stack 12 | cards: 13 | - type: custom:button-card 14 | color_type: card 15 | entity: sensor.security_status 16 | name: "Статус безопасности" 17 | aspect_ratio: 4/1 18 | layout: icon_name 19 | state: 20 | - value: "Safe" 21 | color: green 22 | icon: mdi:shield-check 23 | - value: "Door Opened" 24 | color: orange 25 | icon: mdi:door-open 26 | - default: 27 | color: red 28 | icon: mdi:alert 29 | styles: 30 | card: 31 | - animation: blink 2s ease infinite 32 | - type: custom:auto-entities 33 | show_empty: false 34 | unique: true 35 | card: 36 | type: entities 37 | show_header_toggle: false 38 | filter: 39 | include: 40 | - entity_id: "binary_sensor.front_door" 41 | options: 42 | secondary_info: last-changed 43 | - entity_id: "binary_sensor.*_motion" 44 | options: 45 | type: custom:multiple-entity-row 46 | #entity: this.entity_id 47 | show_state: false 48 | state_color: true 49 | secondary_info: last-changed 50 | entities: 51 | - entity: this.entity_id 52 | name: false 53 | # - attribute: illuminance 54 | # name: "Освещ." 55 | # unit: "lx" 56 | # hide_unavailable: true 57 | - attribute: battery 58 | name: "Заряд" 59 | unit: "%" 60 | hide_unavailable: true 61 | exclude: 62 | - entity_id: "*_1_*" 63 | - entity_id: "*_2_*" 64 | 65 | - type: picture-glance 66 | title: "Последний посетитель" 67 | entities: [] 68 | camera_image: camera.front_door_last_ding 69 | - type: picture-glance 70 | title: "Последнее движение у двери" 71 | entities: [] 72 | camera_image: camera.front_door_last_motion 73 | - type: picture-glance 74 | title: "Сейчас у двери" 75 | entities: [] 76 | camera_image: camera.front_door_live 77 | camera_view: live 78 | 79 | - !include cards/dev.yaml 80 | 81 | -------------------------------------------------------------------------------- /lovelace/90_settings_view.yaml: -------------------------------------------------------------------------------- 1 | title: Settings 2 | icon: mdi:cogs 3 | path: settings 4 | panel: true 5 | cards: 6 | - type: custom:layout-card 7 | layout_type: custom:horizontal-layout 8 | layout: 9 | max_cols: 3 10 | cards: 11 | # - type: custom:auto-entities 12 | # unique: true 13 | # card: 14 | # type: entities 15 | # head: 16 | # type: section 17 | # label: "Потребляемая мощность" 18 | # filter: 19 | # include: 20 | # - entity_id: sensor.daily_energy_consumption 21 | # - entity_id: sensor.monthly_energy_consumption 22 | # - entity_id: sensor.total_energy_consumption 23 | # - entity_id: "*.daily_energy_consumption_*" 24 | # - entity_id: "*.monthly_energy_consumption_*" 25 | # - entity_id: "*_power" 26 | # # exclude: 27 | # # - entity_id: "*_2" 28 | # # - entity_id: "*_management" 29 | 30 | # - type: custom:layout-break 31 | - type: custom:auto-entities 32 | show_empty: false 33 | unique: true 34 | card: 35 | type: entities 36 | title: "Shelly need update" 37 | show_header_toggle: false 38 | filter: 39 | include: 40 | - entity_id: '*firmware_update*' 41 | 42 | - type: vertical-stack 43 | cards: 44 | - type: custom:auto-entities 45 | show_empty: false 46 | unique: true 47 | card: 48 | type: entities 49 | title: "Things that are probably broken" 50 | show_header_toggle: false 51 | filter: 52 | include: 53 | - state: "unknown" 54 | - state: "unavailable" 55 | exclude: 56 | - domain: group 57 | - domain: input_text 58 | - domain: history_graph 59 | - entity_id: "media_player.emby_*" 60 | 61 | # - type: custom:layout-break 62 | - type: custom:auto-entities 63 | show_empty: false 64 | unique: true 65 | card: 66 | type: entities 67 | title: "Батарейки" 68 | show_header_toggle: false 69 | filter: 70 | include: 71 | - group: group.battery_status 72 | - type: custom:auto-entities 73 | unique: true 74 | card: 75 | type: entities 76 | title: "Battery warning" 77 | filter: 78 | include: 79 | - group: group.battery_alert 80 | 81 | # - type: custom:layout-break 82 | - type: vertical-stack 83 | cards: 84 | - !include cards/zigbee.yaml 85 | - !include cards/links.yaml 86 | - !include cards/dev_content.yaml 87 | 88 | -------------------------------------------------------------------------------- /lovelace/cards/dev.yaml: -------------------------------------------------------------------------------- 1 | type: conditional 2 | conditions: 3 | - entity: input_boolean.maintenance_mode 4 | state: "on" 5 | card: 6 | type: vertical-stack 7 | cards: 8 | - type: picture 9 | image: /local/lovelace/bg_maintenance.png 10 | - !include dev_content.yaml 11 | -------------------------------------------------------------------------------- /lovelace/cards/dev_content.yaml: -------------------------------------------------------------------------------- 1 | type: vertical-stack 2 | cards: 3 | - type: horizontal-stack 4 | cards: 5 | - type: custom:button-card 6 | entity: input_boolean.maintenance_mode 7 | color: rgb(117, 189, 111) 8 | color_type: card 9 | show_name: False 10 | state: 11 | - value: "off" 12 | color: rgba(255, 255, 255, 0.1) 13 | - type: custom:button-card 14 | icon: mdi:format-list-bulleted-type 15 | color: rgb(117, 189, 111) 16 | color_type: card 17 | show_name: False 18 | tap_action: 19 | action: url 20 | url: !secret logs_viewer_url 21 | - type: custom:button-card 22 | icon: mdi:clipboard-check-outline 23 | color: rgb(223, 255, 97) 24 | color_type: card 25 | show_name: False 26 | tap_action: 27 | action: call-service 28 | service: homeassistant.check_config 29 | - type: custom:button-card 30 | icon: mdi:reload 31 | color: rgb(255, 97, 97) 32 | color_type: card 33 | show_name: False 34 | tap_action: 35 | action: call-service 36 | service: homeassistant.restart 37 | - type: entities 38 | show_header_toggle: false 39 | entities: 40 | - input_text.git_commit_message 41 | - type: horizontal-stack 42 | cards: 43 | - type: custom:button-card 44 | icon: mdi:github 45 | name: "Repo" 46 | color: rgb(117, 189, 111) 47 | color_type: card 48 | layout: icon_name 49 | tap_action: 50 | action: url 51 | url: https://github.com/Limych/HomeAssistantConfiguration 52 | - type: custom:button-card 53 | icon: mdi:arrow-top-right 54 | name: "Commit" 55 | color: rgb(117, 189, 111) 56 | color_type: card 57 | layout: icon_name 58 | tap_action: 59 | action: call-service 60 | service: shell_command.git_commit 61 | -------------------------------------------------------------------------------- /lovelace/cards/divider_card.yaml: -------------------------------------------------------------------------------- 1 | type: picture 2 | image: /local/lovelace/divider.png 3 | -------------------------------------------------------------------------------- /lovelace/cards/empty.yaml: -------------------------------------------------------------------------------- 1 | type: picture-entity 2 | entity: sun.sun 3 | image: /local/lovelace/bg_transparent.png 4 | show_name: false 5 | show_state: false 6 | tap_action: 7 | action: none 8 | -------------------------------------------------------------------------------- /lovelace/cards/empty_card.yaml: -------------------------------------------------------------------------------- 1 | type: picture-entity 2 | entity: sun.sun 3 | image: /local/lovelace/bg_transparent.png 4 | show_name: false 5 | show_state: false 6 | tap_action: 7 | action: none 8 | -------------------------------------------------------------------------------- /lovelace/cards/laundry.yaml: -------------------------------------------------------------------------------- 1 | type: vertical-stack 2 | cards: 3 | - type: horizontal-stack 4 | cards: 5 | - type: custom:button-card 6 | entity: input_select.washer_status 7 | icon: mdi:washing-machine 8 | color_type: card 9 | color: var(--paper-card-background-color) 10 | state: 11 | - value: "Работает" 12 | color: var(--paper-toggle-button-checked-button-color) 13 | - value: "Очистка" 14 | color: var(--paper-toggle-button-unchecked-button-color) 15 | styles: 16 | card: 17 | - animation: blink 1s ease infinite 18 | show_name: false 19 | tap_action: 20 | action: call-service 21 | service: automation.trigger 22 | service_data: 23 | entity_id: automation.house_washer_mode_idle 24 | - type: custom:button-card 25 | icon: mdi:tumble-dryer 26 | color: var(--paper-toggle-button-checked-button-color) 27 | color_off: var(--paper-toggle-button-unchecked-button-color) 28 | color_type: card 29 | entity: input_boolean.bathroom_drying 30 | show_name: false 31 | action: toggle 32 | - type: horizontal-stack 33 | cards: 34 | - type: custom:bignumber-card 35 | entity: sensor.washing_ontime 36 | title: "Прошло времени" 37 | scale: 15px 38 | - type: custom:bignumber-card 39 | entity: sensor.bathroom_drying_ontime 40 | title: "Прошло времени" 41 | scale: 15px 42 | - type: horizontal-stack 43 | cards: 44 | # - !include empty.yaml 45 | - type: custom:bar-card 46 | entity: sensor.bathroom_humidity 47 | 48 | -------------------------------------------------------------------------------- /lovelace/cards/speakers.yaml: -------------------------------------------------------------------------------- 1 | type: vertical-stack 2 | cards: 3 | - type: entities 4 | show_header_toggle: false 5 | style: | 6 | ha-card { 7 | background-image: url("/local/lovelace/home/bg_sound.jpg"); 8 | background-size: cover; 9 | background-position: center; 10 | } 11 | entities: 12 | - type: custom:mini-media-player 13 | entity: media_player.kitchen 14 | group: true 15 | source: icon 16 | info: short 17 | hide: 18 | icon: true 19 | power: true 20 | # source: true 21 | speaker_group: 22 | platform: linkplay 23 | show_group_count: true 24 | entities: 25 | - entity_id: media_player.kitchen 26 | name: "Кухня" 27 | - entity_id: media_player.living_room 28 | name: "Гостиная" 29 | # - type: custom:mini-media-player 30 | # entity: media_player.kitchen 31 | # name: "Кухня" 32 | # group: true 33 | # source: icon 34 | # hide: 35 | # icon: true 36 | # power: true 37 | # # source: true 38 | # - type: custom:mini-media-player 39 | # entity: media_player.living_room 40 | # name: "Гостиная" 41 | # group: true 42 | # source: icon 43 | # hide: 44 | # icon: true 45 | # power: true 46 | # # source: true 47 | - type: custom:button-card 48 | name: "Фоновая громкость" 49 | icon: mdi:volume-high 50 | size: 80% 51 | color: rgb(117, 111, 189) 52 | color_type: card 53 | layout: icon_name 54 | tap_action: 55 | action: call-service 56 | service: automation.trigger 57 | service_data: 58 | entity_id: automation.media_set_base_volume 59 | styles: 60 | card: 61 | - height: 50px 62 | - type: custom:button-card 63 | entity: input_boolean.play_radio 64 | color: rgb(117, 189, 111) 65 | color_type: card 66 | layout: icon_name 67 | # show_name: False 68 | styles: 69 | card: 70 | - height: 50px 71 | state: 72 | - value: "off" 73 | color: rgba(255, 255, 255, 0.3) 74 | - input_select.radio_station 75 | -------------------------------------------------------------------------------- /lovelace/cards/weather_windy.yaml: -------------------------------------------------------------------------------- 1 | type: iframe 2 | url: "https://embed.windy.com/embed2.html?lat=55.644&lon=37.658&zoom=9&level=surface&overlay=rain&menu=&message=true&marker=&calendar=now&pressure=&type=map&location=coordinates&detail=&detailLat=55.602&detailLon=37.713&metricWind=default&metricTemp=default&radarRange=-1" 3 | -------------------------------------------------------------------------------- /lovelace/cards/zigbee.yaml: -------------------------------------------------------------------------------- 1 | title: "Zigbee" 2 | type: entities 3 | entities: 4 | - entity: input_boolean.zigbee_permit_join 5 | - entity: timer.zigbee_permit_join 6 | - entity: sensor.bridge_state 7 | show_header_toggle: false 8 | -------------------------------------------------------------------------------- /packages/areas/bathroom.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize_glob: 4 | sensor.bathroom_*: 5 | friendly_name: "Ванная" 6 | binary_sensor.bathroom_*: 7 | friendly_name: "Ванная" 8 | 9 | 10 | iaquk: 11 | bathroom: 12 | sources: 13 | temperature: sensor.bathroom_temperature 14 | humidity: sensor.bathroom_humidity 15 | 16 | -------------------------------------------------------------------------------- /packages/areas/bedroom.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize_glob: 4 | sensor.bedroom_*: 5 | friendly_name: "Спальня" 6 | binary_sensor.bedroom_*: 7 | friendly_name: "Спальня" 8 | 9 | 10 | jq300: 11 | username: !secret jq300_username 12 | password: !secret jq300_password 13 | # devices: none 14 | # receive_tvoc_in_ppb: true 15 | # receive_hcho_in_ppb: true 16 | 17 | 18 | iaquk: 19 | bedroom: 20 | sources: 21 | humidity: sensor.bedroom_humidity 22 | co2: sensor.bedroom_eco2 23 | tvoc: sensor.bedroom_tvoc 24 | hcho: sensor.bedroom_hcho 25 | pm: sensor.bedroom_pm25 26 | 27 | 28 | media_player: 29 | - platform: androidtv 30 | name: Bedroom TV 31 | device_class: androidtv 32 | host: 192.168.1.82 33 | state_detection_rules: 34 | 'com.plexapp.android': 35 | - 'paused': 36 | 'media_session_state': 3 # this indentation is important! 37 | 'wake_lock_size': 1 # this indentation is important! 38 | - 'playing': 39 | 'media_session_state': 3 # this indentation is important! 40 | - 'standby' 41 | 42 | -------------------------------------------------------------------------------- /packages/areas/hallway.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize: 4 | binary_sensor.front_door: 5 | device_class: door 6 | friendly_name: "Входная дверь" 7 | binary_sensor.hallway_motion: 8 | friendly_name: "Коридор" 9 | 10 | 11 | binary_sensor: 12 | - platform: rpi_gpio 13 | pull_mode: 'DOWN' 14 | ports: 15 | 23: "Hallway Motion" 16 | - platform: rpi_gpio 17 | pull_mode: 'DOWN' 18 | invert_logic: true 19 | ports: 20 | 24: "Front Door" 21 | 22 | -------------------------------------------------------------------------------- /packages/areas/living_room.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize_glob: 4 | sensor.living_room_*: 5 | friendly_name: "Гостиная" 6 | binary_sensor.living_room_*: 7 | friendly_name: "Гостиная" 8 | 9 | -------------------------------------------------------------------------------- /packages/car.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize: 4 | input_boolean.car_clean: 5 | templates: 6 | icon: >- 7 | if (state == 'on') return 'mdi:car'; 8 | return 'mdi:car-wash'; 9 | binary_sensor.car_wash: 10 | templates: 11 | icon: >- 12 | if (state == 'on') return 'mdi:weather-sunny'; 13 | return 'mdi:weather-pouring'; 14 | 15 | 16 | input_boolean: 17 | car_clean: 18 | name: "Машина чистая" 19 | 20 | 21 | binary_sensor: 22 | - platform: car_wash 23 | weather: weather.gismeteo_daily 24 | 25 | - platform: snowtire 26 | weather: weather.gismeteo_daily 27 | 28 | 29 | sensor: 30 | - platform: template 31 | sensors: 32 | car_wash_state: 33 | value_template: >- 34 | {% if is_state('input_boolean.car_clean', 'on') %} 35 | Clean 36 | {% elif is_state('binary_sensor.car_wash', 'off') %} 37 | Bad Weather 38 | {% else %} 39 | Time to Wash 40 | {% endif %} 41 | 42 | 43 | automation: 44 | - alias: '[Misc] When I can Clear the Car' 45 | initial_state: on 46 | trigger: 47 | - entity_id: sensor.car_wash_state 48 | platform: state 49 | to: 'Time to Wash' 50 | action: 51 | - service: notify.limych 52 | data_template: 53 | message: "Пора запланировать мойку машины." 54 | 55 | - alias: '[Misc] Time to Change Car Tires' 56 | initial_state: on 57 | trigger: 58 | - platform: state 59 | entity_id: binary_sensor.snowtire 60 | action: 61 | - service: notify.limych 62 | data_template: 63 | message: "Пора менять резину на {{ 'ЗИМНЮЮ' if is_state('binary_sensor.snowtire', 'on') else 'ЛЕТНЮЮ' }}" 64 | 65 | -------------------------------------------------------------------------------- /packages/device_tracker.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | device_tracker: 3 | # Classical method 4 | - platform: nmap_tracker 5 | interval_seconds: 30 6 | consider_home: 180 7 | hosts: !secret nmap_tracker_hosts 8 | scan_options: "-sn -T5 -n" 9 | home_interval: 3 10 | new_device_defaults: 11 | track_new_devices: true 12 | exclude: 13 | - !secret hass_host_ip 14 | 15 | # Modern and more accurate method but need install of custom component 16 | - platform: arpscan_tracker 17 | interval_seconds: 15 18 | consider_home: 180 19 | scan_options: "--interface=eth0 -l -g -t1 -q" 20 | exclude: 21 | - !secret hass_host_ip 22 | 23 | # Most accurate method but need some config changes on router. 24 | # Also it can scan many subnets. 25 | - platform: snmp 26 | # Set net.link.ether.inet.max_age=180 (ARP timeout) 27 | # On OPNsense: System > Settins > Tunables 28 | # and install os-net-snmp plugin 29 | interval_seconds: 12 30 | consider_home: 60 31 | host: !secret gateway_host_ip 32 | community: public 33 | baseoid: 1.3.6.1.2.1.4.22.1.2 34 | 35 | #- platform: google_maps 36 | # username: !secret google_location_username 37 | # password: !secret google_location_password 38 | # max_gps_accuracy: 150 39 | 40 | 41 | 42 | automation: 43 | - alias: '[System] New Device Tracked' 44 | initial_state: on 45 | trigger: 46 | platform: event 47 | event_type: device_tracker_new_device 48 | action: 49 | - service: notify.hass_info 50 | data_template: 51 | title: "New Device Tracked" 52 | message: "{{trigger.event.data.host_name}} ({{trigger.event.data.mac}})" 53 | 54 | -------------------------------------------------------------------------------- /packages/house/cleaning_day.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | input_boolean: 3 | cleaning_day: 4 | name: "День Уборки" 5 | icon: mdi:broom 6 | 7 | -------------------------------------------------------------------------------- /packages/house/guest_mode.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize: 4 | input_boolean.guest_mode: 5 | templates: 6 | # icon: > 7 | # if (state === 'on') return 'mdi:account-group'; 8 | # else if (state === 'off') return 'mdi:account-multiple-minus'; 9 | icon_color: > 10 | if (state === 'on') return 'rgb(56, 150, 56)'; 11 | else if (state === 'off') return 'rgb(249, 251, 255)'; 12 | 13 | 14 | input_boolean: 15 | guest_mode: 16 | name: "Гости в Доме" 17 | icon: mdi:account-clock 18 | 19 | 20 | automation: 21 | - alias: '[House] Guest Mode On by Presence' 22 | initial_state: on 23 | trigger: 24 | - platform: state 25 | entity_id: >- 26 | device_tracker.guest_ivanchev_phone1, device_tracker.guest_ivanchev_phone2, 27 | device_tracker.guest_ivanchev_notebook, device_tracker.guest_ivanchev_notebook2, 28 | device_tracker.guest_tolya_phone, 29 | device_tracker.guest_kent_notebook, 30 | device_tracker.guest_iseeyou_phone, device_tracker.guest_iseeyou_laptop 31 | to: home 32 | action: 33 | - service: homeassistant.turn_on 34 | entity_id: input_boolean.guest_mode 35 | - service: notify.all 36 | data_template: 37 | title: 'Guest mode on' 38 | message: > 39 | 'Встречайте {{ trigger.to_state.name }}' 40 | 41 | - alias: '[House] Guest Mode On' 42 | initial_state: on 43 | trigger: 44 | - platform: state 45 | entity_id: input_boolean.guest_mode 46 | from: 'off' 47 | to: 'on' 48 | action: 49 | - service: notify.all 50 | data_template: 51 | title: 'Guest mode on' 52 | message: > 53 | 'Гости в доме!' 54 | 55 | - alias: '[House] Guest Mode Off' 56 | initial_state: on 57 | trigger: 58 | - platform: state 59 | entity_id: input_boolean.guest_mode 60 | from: 'on' 61 | to: 'off' 62 | action: 63 | - service: notify.all 64 | data_template: 65 | title: 'Guest mode off' 66 | message: > 67 | 'Гости ушли к себе…' 68 | 69 | -------------------------------------------------------------------------------- /packages/house/shelly.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | shelly: 3 | devices: #devices to be added 4 | - id: "7A3507" 5 | name: "Washer Shelly Plug" 6 | entity_id: washer_plug 7 | - id: "7ADF96" 8 | name: "Server Shelly Plug" 9 | entity_id: server_plug 10 | - id: "040926" 11 | name: "Kitchen Fan Shelly Plug" 12 | entity_id: kitchen_fan_plug 13 | - id: "691FEA-1" 14 | name: "Kitchen Tabletop Light" 15 | light_switch: true 16 | 17 | -------------------------------------------------------------------------------- /packages/interactive/README.md: -------------------------------------------------------------------------------- 1 | # Interactive 2 | 3 | The telegram interactions in this folder were inspired by Masterkenobi - https://community.home-assistant.io/users/masterkenobi 4 | 5 | The notifications system was inspired by Lentron's "Janet" project - https://community.home-assistant.io/t/janet-the-good-place/38904 - and CCostan's notification briefings/scripts - https://github.com/CCOSTAN/Home-AssistantConfig/tree/master/script 6 | -------------------------------------------------------------------------------- /packages/security/alerts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | sensor: 3 | - platform: template 4 | sensors: 5 | security_status: 6 | value_template: >- 7 | {% if is_state('binary_sensor.front_door', 'on') %} 8 | Door Opened 9 | {% elif is_state('group.motion', 'off') 10 | or is_state('binary_sensor.presence_owners', 'on') %} 11 | Safe 12 | {% else %} 13 | Unsafe 14 | {% endif %} 15 | icon_template: 16 | mdi:shield-home 17 | 18 | 19 | input_boolean: 20 | security_show_more: 21 | initial: false 22 | 23 | 24 | script: 25 | front_door_snapshot: 26 | sequence: 27 | - service: camera.snapshot 28 | data: 29 | entity_id: camera.front_door_live 30 | filename: /config/www/images/frontdoor.jpg 31 | - service: notify.all 32 | data: 33 | message: "Входная дверь открыта!" 34 | data: 35 | photo: 36 | file: /config/www/images/frontdoor.jpg 37 | caption: "Входная дверь открыта!" 38 | 39 | 40 | automation: 41 | - alias: '[Security] Front Door Opened' 42 | initial_state: on 43 | trigger: 44 | - platform: state 45 | entity_id: 46 | - binary_sensor.front_door 47 | to: 'on' 48 | condition: 49 | condition: or 50 | conditions: 51 | - condition: state 52 | entity_id: binary_sensor.presence_owners 53 | state: 'off' 54 | - condition: state 55 | entity_id: input_boolean.everyone_is_asleep 56 | state: 'on' 57 | action: 58 | - service: script.front_door_snapshot 59 | - delay: 1 60 | - repeat: 61 | while: 62 | - condition: state 63 | entity_id: binary_sensor.front_door 64 | state: 'on' 65 | sequence: 66 | - service: script.front_door_snapshot 67 | - delay: 5 68 | 69 | -------------------------------------------------------------------------------- /packages/security/cameras.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize: 4 | camera.intercom: 5 | friendly_name: "Входная дверь" 6 | binary_sensor.front_door_motion: 7 | friendly_name: "Перед входной дверью" 8 | 9 | 10 | beward: 11 | - host: !secret camera_doorbell_host 12 | username: !secret camera_doorbell_login 13 | password: !secret camera_doorbell_password 14 | name: "Front door" 15 | stream: 1 16 | binary_sensors: 17 | - online 18 | - motion 19 | - ding 20 | sensors: 21 | - last_activity 22 | - last_motion 23 | - last_ding 24 | 25 | 26 | automation: 27 | - alias: "[Security] Front door Ding" 28 | initial_state: on 29 | trigger: 30 | platform: state 31 | entity_id: binary_sensor.front_door_ding 32 | to: 'on' 33 | action: 34 | - delay: 2 35 | - service: notify.limych 36 | data: 37 | message: "Звонят в дверь!" 38 | data: 39 | photo: 40 | file: /config/.storage/beward/front_door_last_ding.jpg 41 | caption: "Звонят в дверь!" 42 | 43 | -------------------------------------------------------------------------------- /packages/security/ip_ban.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | automation: 3 | - alias: '[Security] IP Ban Notify' 4 | initial_state: on 5 | trigger: 6 | - platform: state 7 | entity_id: persistent_notification.ipban 8 | condition: 9 | - condition: template 10 | value_template: "{{ trigger.to_state.state != off }}" 11 | action: 12 | - service: notify.hass_info 13 | data_template: 14 | title: "IP address has been banned!" 15 | message: "{{ trigger.state.attributes.message }}" 16 | 17 | -------------------------------------------------------------------------------- /packages/system/clean_temp.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | shell_command: 3 | clean_temp: 'find /tmp -type f -mtime +7 -exec rm {} \;' 4 | 5 | automation: 6 | - alias: "[System] Clean temp" 7 | initial_state: on 8 | trigger: 9 | platform: sun 10 | event: sunrise 11 | action: 12 | service: shell_command.clean_temp 13 | 14 | -------------------------------------------------------------------------------- /packages/system/devices_monitor.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize_glob: 4 | sensor.*battery*: 5 | templates: 6 | icon_color: |- 7 | if (state > 75) return 'hsl(100, 100%, 50%)' 8 | else if (state > 50) return 'hsl(60, 100%, 50%)' 9 | else if (state > 25) return 'hsl(35, 100%, 50%)' 10 | else if (state <= 25) return 'hsl(0, 100%, 50%)' 11 | else return 'var(--disabled-text-color)' 12 | 13 | sensor.*_pc_*: 14 | icon: mdi:laptop 15 | templates: 16 | icon_color: |- 17 | if (state === 'Online') return 'var(--paper-item-icon-color)' 18 | else return 'var(--disabled-text-color)' 19 | 20 | -------------------------------------------------------------------------------- /packages/system/git_backup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | input_text: 3 | git_commit_message: 4 | name: "Commit Message" 5 | initial: "Commit from Hassio" 6 | 7 | 8 | shell_command: 9 | git_commit: '/usr/bin/env sh /config/bin/git_commit.sh "{{ states.input_text.git_commit_message.state }}"' 10 | git_fetch: '/usr/bin/env sh /config/bin/git_fetch.sh' 11 | # git_auto_fetch: '/usr/bin/env sh /config/bin/check_ci_status.sh && /usr/bin/env sh /config/bin/git_fetch.sh' 12 | 13 | 14 | # automation: 15 | # - alias: "[System] Autoupdate configs from GitHub" 16 | # initial_state: on 17 | # trigger: 18 | # platform: state 19 | # entity_id: binary_sensor.travis_ci_status 20 | # from: 'off' 21 | # to: 'on' 22 | # action: 23 | # - service: notify.hass_info 24 | # data: 25 | # title: 'Travis-CI' 26 | # message: 'Build was successful' 27 | # - service: shell_command.git_fetch 28 | 29 | # - alias: "[System] Autoupdate configs from GitHub" 30 | # initial_state: on 31 | # trigger: 32 | # platform: time_pattern 33 | # minutes: '/10' 34 | # action: 35 | # service: shell_command.git_auto_fetch 36 | 37 | -------------------------------------------------------------------------------- /packages/system/internet_monitor.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | homeassistant: 3 | customize: 4 | binary_sensor.internet: 5 | friendly_name: "Интернет" 6 | templates: 7 | icon_color: |- 8 | if (state === 'on') return 'var(--paper-item-icon-color)' 9 | else return 'var(--accent-color)' 10 | 11 | 12 | sensor: 13 | - platform: dnsip 14 | 15 | 16 | binary_sensor: 17 | - platform: ping 18 | name: internet 19 | host: yandex.ru 20 | scan_interval: 10 21 | count: 3 22 | 23 | 24 | speedtestdotnet: 25 | scan_interval: 26 | hours: 3 27 | 28 | -------------------------------------------------------------------------------- /packages/system/zigbee2mqtt.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | input_boolean: 3 | zigbee_permit_join: 4 | name: "Allow devices to join" 5 | initial: off 6 | icon: mdi:cellphone-wireless 7 | 8 | timer: 9 | zigbee_permit_join: 10 | name: "Time remaining" 11 | duration: 600 # Updated this to the number of seconds you wish 12 | 13 | sensor: 14 | - platform: mqtt 15 | name: "Bridge state" 16 | state_topic: "zigbee2mqtt/bridge/state" 17 | icon: mdi:router-wireless 18 | 19 | automation: 20 | - id: enable_zigbee_join 21 | alias: "[System] Enable Zigbee joining" 22 | trigger: 23 | - platform: state 24 | entity_id: input_boolean.zigbee_permit_join 25 | to: 'on' 26 | action: 27 | - service: mqtt.publish 28 | data: 29 | topic: zigbee2mqtt/bridge/config/permit_join 30 | payload: 'true' 31 | - service: timer.start 32 | data: 33 | entity_id: timer.zigbee_permit_join 34 | 35 | - id: disable_zigbee_join 36 | alias: "[System] Disable Zigbee joining" 37 | trigger: 38 | - entity_id: input_boolean.zigbee_permit_join 39 | platform: state 40 | to: 'off' 41 | - platform: event 42 | event_type: timer.finished 43 | event_data: 44 | entity_id: timer.zigbee_permit_join 45 | action: 46 | - service: mqtt.publish 47 | data: 48 | payload: 'false' 49 | topic: zigbee2mqtt/bridge/config/permit_join 50 | - service: timer.cancel 51 | data: 52 | entity_id: timer.zigbee_permit_join 53 | - service: input_boolean.turn_off 54 | data: 55 | entity_id: input_boolean.zigbee_permit_join 56 | 57 | 58 | webhook: 59 | 60 | -------------------------------------------------------------------------------- /packages/temp_light_humidity.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | binary_sensor: 3 | - platform: template 4 | sensors: 5 | dark_outside: 6 | friendly_name: "Темно на улице" 7 | value_template: >- 8 | {{ is_state('sun.sun', 'below_horizon') }} 9 | 10 | -------------------------------------------------------------------------------- /packages/tts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | tts: 3 | - platform: google_translate 4 | language: 'ru' 5 | 6 | 7 | 8 | automation: 9 | - alias: "[Media] TTS test" 10 | initial_state: on 11 | trigger: 12 | - platform: state 13 | entity_id: input_select.day_mode 14 | to: 'Утро' 15 | action: 16 | - service: media_player.media_pause 17 | entity_id: media_player.kitchen 18 | - service: media_player.volume_set 19 | data: 20 | entity_id: media_player.kitchen 21 | volume_level: 0.50 22 | - service: tts.google_translate_say 23 | entity_id: media_player.kitchen 24 | data: 25 | message: "С добрым утром!" 26 | - service: automation.trigger 27 | entity_id: automation.media_set_base_volume 28 | 29 | -------------------------------------------------------------------------------- /packages/vacation_mode.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | input_boolean: 3 | vacation_mode: 4 | name: "Режим Отпуска" 5 | icon: mdi:beach 6 | 7 | 8 | automation: 9 | - alias: '[House] Vacation Mode Notify' 10 | initial_state: on 11 | trigger: 12 | - platform: state 13 | entity_id: input_boolean.vacation_mode 14 | action: 15 | - service: notify.limych 16 | data_template: 17 | message: "Режим Отпуска переключен в {{ trigger.to_state.state | upper }}" 18 | 19 | - alias: '[House] Vacation Mode On' 20 | initial_state: on 21 | trigger: 22 | - platform: state 23 | entity_id: binary_sensor.presence_owners 24 | to: 'off' 25 | for: 26 | hours: 24 27 | action: 28 | - service: input_boolean.turn_on 29 | entity_id: input_boolean.vacation_mode 30 | 31 | - alias: '[House] Vacation Mode Off' 32 | initial_state: on 33 | trigger: 34 | - platform: state 35 | entity_id: binary_sensor.presence_owners 36 | to: 'on' 37 | action: 38 | - service: input_boolean.turn_off 39 | entity_id: input_boolean.vacation_mode 40 | 41 | -------------------------------------------------------------------------------- /packages/weather.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | weather: 3 | - platform: gismeteo 4 | - platform: gismeteo 5 | name: "Gismeteo Daily" 6 | mode: daily 7 | 8 | 9 | sensor: 10 | # Gismeteo sensors 11 | - platform: gismeteo 12 | forecast: true 13 | monitored_conditions: 14 | - condition 15 | - temperature 16 | - temperature_feels_like 17 | - wind_speed 18 | - wind_bearing 19 | - humidity 20 | - pressure 21 | - clouds 22 | - rain 23 | - snow 24 | - storm 25 | - geomagnetic 26 | - water_temperature 27 | 28 | 29 | - platform: average 30 | name: 'Average Temperature' 31 | duration: 32 | days: 3 33 | entities: 34 | - sensor.gismeteo_temperature 35 | - sensor.narodmon_temperature 36 | 37 | -------------------------------------------------------------------------------- /scenes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limych/HomeAssistantConfiguration/758ab1c012a2e7a705939abd9bbadbb0a3f5a688/scenes/.gitkeep -------------------------------------------------------------------------------- /tests/example.com.fake_crt: -------------------------------------------------------------------------------- 1 | DUMMY 2 | -------------------------------------------------------------------------------- /tests/example.com.fake_key: -------------------------------------------------------------------------------- 1 | DUMMY key 2 | -------------------------------------------------------------------------------- /tests/markdown.style.rb: -------------------------------------------------------------------------------- 1 | rule "MD013", :code_blocks => false, :tables => false -------------------------------------------------------------------------------- /ui-lovelace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | custom_header: 3 | compact_mode: true 4 | title: Home 5 | cch: 6 | menu: show 7 | notifications: show 8 | voice: hide 9 | options: clock 10 | clock_format: 24 11 | swipe: true 12 | swipe_animate: swipe 13 | preload_cards: 14 | - conditional 15 | - custom:vertical-style-card 16 | views: 17 | - !include lovelace/00_home_view.yaml 18 | - !include lovelace/10_weather_view.yaml 19 | - !include lovelace/20_home_info_view.yaml 20 | - !include lovelace/30_lights_view.yaml 21 | - !include lovelace/40_system_info_view.yaml 22 | # - !include lovelace/41_system_graphs.yaml 23 | - !include lovelace/50_security_view.yaml 24 | # - !include lovelace/60_traffic_view.yaml 25 | # - !include lovelace/areas.yaml 26 | 27 | - !include lovelace/90_settings_view.yaml 28 | - !include lovelace/99_test_view.yaml 29 | 30 | --------------------------------------------------------------------------------