├── .HA_VERSION ├── .gitignore ├── LICENSE ├── README.md ├── automations.yaml ├── blueprints ├── automation │ └── homeassistant │ │ ├── motion_light.yaml │ │ └── notify_leaving_zone.yaml └── script │ └── homeassistant │ └── confirmable_notification.yaml ├── configuration.yaml ├── covers ├── 3DPrintingRoomBlinds.yaml ├── KitchenBlinds.yaml ├── LivingRoomBlinds.yaml ├── MasterBedroomBlinds.yaml ├── PrimaryGarageDoor.yaml └── SecondaryGarageDoor.yaml ├── custom_components ├── alexa_media │ ├── .translations │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── nb.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt_BR.json │ │ ├── pt_PT.json │ │ ├── ru.json │ │ └── zh-Hans.json │ ├── __init__.py │ ├── alarm_control_panel.py │ ├── alexa_entity.py │ ├── alexa_media.py │ ├── config_flow.py │ ├── const.py │ ├── helpers.py │ ├── light.py │ ├── manifest.json │ ├── media_player.py │ ├── notify.py │ ├── sensor.py │ ├── services.py │ ├── services.yaml │ ├── strings.json │ ├── switch.py │ └── translations │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── nb.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt_BR.json │ │ ├── pt_PT.json │ │ ├── ru.json │ │ └── zh-Hans.json ├── 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 └── 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 ├── customize.yaml ├── groups.yaml ├── harmony_10977089.conf ├── image ├── 60b08011c64f76b31565000dccd7186b │ ├── 512x512 │ └── original ├── 984422fe027bc3759750871a0b631c59 │ ├── 512x512 │ └── original └── edc1f990c6e31085b7bba1bee8b888f3 │ ├── 512x512 │ └── original ├── lights ├── HolidayLights.yaml ├── HolidayLightsColor1.yaml ├── HolidayLightsColor2.yaml ├── HolidayLightsColor3.yaml ├── HolidayLightsGlitter.yaml ├── HolidayLightsLightning.yaml ├── OfficeLEDBulbs.yaml ├── Shelly3DPrinterRoomLightRelay.yaml ├── ShellyDownstairsBathroomLightRelay.yaml ├── ShellyMasterBathroomMainLightRelay.yaml └── ShellyMasterBathroomVentLightRelay.yaml ├── scenes.yaml ├── scripts.yaml ├── sensors ├── HolidaySensor.yaml ├── NetData.yaml ├── ShellyHTMasterBathroomBattery.yaml ├── ShellyHTMasterBathroomHumidity.yaml ├── ShellyHTMasterBathroomTemp.yaml ├── TasmotaLatestVersionSensor.yaml ├── WorkComputerWFH.yaml └── systemmonitor.yaml ├── switches ├── AddGlitter.yaml ├── AddLightning.yaml ├── AudioEffects.yaml ├── Shelly3DPrinterRoomFanRelay.yaml ├── ShellyDownstairsBathroomVentRelay.yaml └── ShellyMasterBathroomVentRelay.yaml ├── themes └── midnight │ └── midnight.yaml ├── tts ├── 29dc8ba06b5c447f087b9bcd92b400a1e1ccf2ad_ru-ru_a9c18110b0_cloud.mp3 ├── bfc702d80671b6944df07c65c91b383cec555c8a_en-us_a9c18110b0_cloud.mp3 └── f26a333f608779251c79ad6524c1f8758bb67f01_en-us_a9c18110b0_cloud.mp3 ├── www └── community │ ├── lovelace-auto-entities │ ├── auto-entities.js │ ├── auto-entities.js.gz │ ├── rollup.config.js │ └── rollup.config.js.gz │ ├── lovelace-battery-entity-row │ ├── battery-entity-row.js │ └── battery-entity-row.js.gz │ ├── lovelace-multiple-entity-row │ ├── multiple-entity-row.js │ └── multiple-entity-row.js.gz │ ├── lovelace-template-entity-row │ ├── template-entity-row.js │ ├── template-entity-row.js.gz │ ├── webpack.config.js │ └── webpack.config.js.gz │ ├── mini-graph-card │ ├── mini-graph-card-bundle.js │ └── mini-graph-card-bundle.js.gz │ └── vacuum-card │ ├── vacuum-card.js │ └── vacuum-card.js.gz └── zwave_device_config.yaml /.HA_VERSION: -------------------------------------------------------------------------------- 1 | 2021.5.4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directory (contents) ignores 2 | .cloud 3 | .storage 4 | deps 5 | 6 | # Generic ignores 7 | *.log 8 | *.db 9 | *.db-shm 10 | *.db-wal 11 | *.pyc 12 | *.pickle 13 | ._* 14 | __pycache__ 15 | 16 | 17 | # Specific file ignores 18 | .google.token 19 | .uuid 20 | emulated_hue_ids.json 21 | google_calendars.yaml 22 | known_devices.yaml 23 | options.xml 24 | OZW_Log.txt 25 | pyozw.sqlite 26 | secrets.yaml 27 | webostv.conf 28 | zwcfg_* 29 | zwscene.xml 30 | 31 | 32 | # Ignore files created by IDE's 33 | .vscode 34 | 35 | 36 | # Specific keeps 37 | !.gitkeep 38 | !.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joshua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant Configuration 2 | 3 | ## ⚠️ WARNING: ⚠️ 4 | 5 | ## ⚠️ This repo will be removed soon. It is outdated and does not accurately represent my Home Assistant instance anymore 6 | 7 | ## 💙 Remember Support [NabuCasa](https://www.nabucasa.com/) - Help keep Home Assistant alive and growing! 8 | 9 | --- 10 | 11 | This is my [Home Assistant](https://home-assistant.io/) configuration. I have installed HA on a [HP ProDesk 600 G3 Mini](https://support.hp.com/us-en/document/c05364047). I am running Ubuntu on this machine and running mostly Docker images on it. This includes the [Home Assistant Docker Image](https://hub.docker.com/r/homeassistant/home-assistant/). 12 | 13 | I regularly update my configuration files. You can check my current HA version [here](.HA_VERSION). If you like anything here, be sure to star my repo! 14 | 15 | ## Docker Containers Related to my Home Assistant Instance 16 | 17 | * [Mosquitto Broker](https://hub.docker.com/_/eclipse-mosquitto) - For managing my Sonoff devices and any future MQTT devices (Docker Container) 18 | * [NodeRed](https://hub.docker.com/r/nodered/node-red) - NodeRed for my more complex automations (and maybe all automations in the future. We will see.) 19 | 20 | ## Hardware 21 | 22 | * [HP ProDesk 600 G3 Mini](https://support.hp.com/us-en/document/c05364047) 23 | * [Aeotec Z-Stick Gen5](https://www.amazon.com/dp/B00X0AWA6E/) for Z-Wave control 24 | 25 | ## Components / Devices / Integrations 26 | 27 | * [Abode Home Security](https://home-assistant.io/components/alarm_control_panel.abode/) 28 | * [EcoBee Thermostat](https://www.home-assistant.io/components/ecobee/) 29 | * [Logitech Harmony Hub](https://www.home-assistant.io/integrations/harmony/) 30 | * [Meteorologisk institutt (Met.no)](https://www.home-assistant.io/integrations/met/) 31 | * [Moon](https://www.home-assistant.io/components/sensor.moon/) 32 | * [Network UPS Tools (NUT)](https://www.home-assistant.io/integrations/nut/) 33 | * [OctoPrint](https://www.home-assistant.io/components/octoprint/) 34 | * [Plex Media Server](https://www.home-assistant.io/integrations/plex/) 35 | * [Ring Video Doorbell](https://home-assistant.io/components/ring/) 36 | * [Sonoff](https://sonoff.itead.cc/en/) 37 | * [Synology](https://www.home-assistant.io/integrations/synology/) 38 | * [Template Sensors](https://www.home-assistant.io/components/sensor.template/) 39 | * [Time/Date Sensor](https://www.home-assistant.io/integrations/time_date/) 40 | * [Ubiquiti UniFi](https://www.home-assistant.io/integrations/unifi/) 41 | * [Weather](https://www.home-assistant.io/integrations/weather/) 42 | 43 | ## HACS ([Home Assistant Community Store](https://hacs.xyz/)) 44 | 45 | * [Alexa Media Player](https://github.com/custom-components/alexa_media_player) 46 | * [Midnight Theme](https://github.com/home-assistant-community-themes/midnight) 47 | * [ShellyForHass](https://github.com/StyraHem/ShellyForHASS) (May remove in favor of MQTT/NodeRed) 48 | * [Vacuum Card](https://github.com/denysdovhan/vacuum-card) -------------------------------------------------------------------------------- /automations.yaml: -------------------------------------------------------------------------------- 1 | - id: '1586114387078' 2 | alias: Sunrise Automation 3 | description: '' 4 | trigger: 5 | - event: sunrise 6 | platform: sun 7 | condition: [] 8 | action: 9 | - service: mqtt.publish 10 | data: 11 | topic: NodeRed/routine/sunrise 12 | payload: 'ON' 13 | qos: 1 14 | retain: false 15 | mode: single 16 | - id: '1586116713458' 17 | alias: Sunset Automation (1 Hr Before) 18 | description: '' 19 | trigger: 20 | - event: sunset 21 | offset: -01:00:00 22 | platform: sun 23 | condition: [] 24 | action: 25 | - service: mqtt.publish 26 | data: 27 | topic: NodeRed/routine/sunset1hourbefore 28 | payload: 'ON' 29 | qos: 1 30 | retain: false 31 | mode: single 32 | - id: '1586117279160' 33 | alias: Bed Time Automation 34 | description: '' 35 | trigger: 36 | - entity_id: input_boolean.goodnightmode 37 | platform: state 38 | to: 'on' 39 | condition: [] 40 | action: 41 | - service: mqtt.publish 42 | data: 43 | topic: NodeRed/routine/bedtime 44 | payload: 'ON' 45 | qos: 1 46 | retain: false 47 | mode: single 48 | - id: '1589770574485' 49 | alias: Enable PiHole 50 | description: '' 51 | trigger: 52 | - entity_id: input_boolean.pi_hole 53 | from: 'off' 54 | platform: state 55 | to: 'on' 56 | condition: [] 57 | action: 58 | - data: {} 59 | service: pi_hole.enable 60 | - id: '1589771493250' 61 | alias: Disable PiHole 62 | description: '' 63 | trigger: 64 | - entity_id: input_boolean.pi_hole 65 | from: 'on' 66 | platform: state 67 | to: 'off' 68 | condition: [] 69 | action: 70 | - data: 71 | duration: '9999:00:00' 72 | service: pi_hole.disable 73 | - id: '1595108471898' 74 | alias: Secondary Garage Open Too Long 75 | description: '' 76 | trigger: 77 | - entity_id: sensor.secondary_garage_door_sensor 78 | for: 00:30:00 79 | from: closed 80 | platform: state 81 | to: open 82 | condition: [] 83 | action: 84 | - data: 85 | message: Secondard Garage Open for longer than 30 minutes 86 | service: notify.mobile_app_joshua_pixel_5 87 | mode: single 88 | - id: '1612997864802' 89 | alias: Sunset Automation 90 | description: '' 91 | trigger: 92 | - platform: sun 93 | event: sunset 94 | condition: [] 95 | action: 96 | - service: mqtt.publish 97 | data: 98 | topic: NodeRed/routine/sunset 99 | payload: 'ON' 100 | qos: 1 101 | retain: false 102 | mode: single 103 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /blueprints/script/homeassistant/confirmable_notification.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Confirmable Notification 3 | description: >- 4 | A script that sends an actionable notification with a confirmation before 5 | running the specified action. 6 | domain: script 7 | source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml 8 | input: 9 | notify_device: 10 | name: Device to notify 11 | description: Device needs to run the official Home Assistant app to receive notifications. 12 | selector: 13 | device: 14 | integration: mobile_app 15 | title: 16 | name: "Title" 17 | description: "The title of the button shown in the notification." 18 | default: "" 19 | selector: 20 | text: 21 | message: 22 | name: "Message" 23 | description: "The message body" 24 | selector: 25 | text: 26 | confirm_text: 27 | name: "Confirmation Text" 28 | description: "Text to show on the confirmation button" 29 | default: "Confirm" 30 | selector: 31 | text: 32 | confirm_action: 33 | name: "Confirmation Action" 34 | description: "Action to run when notification is confirmed" 35 | default: [] 36 | selector: 37 | action: 38 | dismiss_text: 39 | name: "Dismiss Text" 40 | description: "Text to show on the dismiss button" 41 | default: "Dismiss" 42 | selector: 43 | text: 44 | dismiss_action: 45 | name: "Dismiss Action" 46 | description: "Action to run when notification is dismissed" 47 | default: [] 48 | selector: 49 | action: 50 | 51 | mode: restart 52 | 53 | sequence: 54 | - alias: "Send notification" 55 | domain: mobile_app 56 | type: notify 57 | device_id: !input notify_device 58 | title: !input title 59 | message: !input message 60 | data: 61 | actions: 62 | - action: "CONFIRM" 63 | title: !input confirm_text 64 | - action: "DISMISS" 65 | title: !input dismiss_text 66 | - alias: "Awaiting response" 67 | wait_for_trigger: 68 | - platform: event 69 | event_type: mobile_app_notification_action 70 | - choose: 71 | - conditions: "{{ wait.trigger.event.data.action == 'CONFIRM' }}" 72 | sequence: !input confirm_action 73 | - conditions: "{{ wait.trigger.event.data.action == 'DISMISS' }}" 74 | sequence: !input dismiss_action 75 | -------------------------------------------------------------------------------- /configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | 3 | homeassistant: 4 | customize: !include customize.yaml 5 | 6 | frontend: 7 | themes: !include_dir_merge_named themes 8 | 9 | recorder: 10 | purge_keep_days: 31 11 | db_url: !secret mariadb_cs 12 | 13 | remote: 14 | - platform: harmony 15 | name: 'Living Room' 16 | host: !secret harmony_hub_ip 17 | activity: Play Xbox 18 | 19 | alexa_media: 20 | accounts: 21 | - email: !secret amazon_user 22 | password: !secret amazon_password 23 | url: amazon.com 24 | 25 | # PiHole 26 | pi_hole: 27 | - host: !secret pihole_ipadress 28 | api_key: !secret pihole_api 29 | 30 | # Text to speech 31 | tts: 32 | - platform: google_translate 33 | 34 | automation: !include automations.yaml 35 | cover: !include_dir_list covers/ 36 | group: !include groups.yaml 37 | light: !include_dir_list lights/ 38 | script: !include scripts.yaml 39 | scene: !include scenes.yaml 40 | sensor: !include_dir_list sensors/ 41 | switch: !include_dir_list switches/ 42 | -------------------------------------------------------------------------------- /covers/3DPrintingRoomBlinds.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "3D Printing Room Blinds" 3 | state_topic: "Blinds009/blindsCommand" 4 | command_topic: "blinds/3dprintroom/blindsCommand" 5 | value_template: > 6 | {% if value == "CLOSE" %} 7 | closed 8 | {% else %} 9 | open 10 | {% endif %} 11 | payload_open: "OPEN" 12 | payload_close: "CLOSE" 13 | retain: false -------------------------------------------------------------------------------- /covers/KitchenBlinds.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Kitchen Blinds" 3 | state_topic: "Blinds001/blindsCommand" 4 | command_topic: "blinds/kitchen/blindsCommand" 5 | value_template: > 6 | {% if value == "CLOSE" %} 7 | closed 8 | {% else %} 9 | open 10 | {% endif %} 11 | payload_open: "OPEN" 12 | payload_close: "CLOSE" 13 | retain: false -------------------------------------------------------------------------------- /covers/LivingRoomBlinds.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Living Room Blinds" 3 | state_topic: "Blinds003/blindsCommand" 4 | command_topic: "blinds/livingroom/blindsCommand" 5 | value_template: > 6 | {% if value == "CLOSE" %} 7 | closed 8 | {% else %} 9 | open 10 | {% endif %} 11 | payload_open: "OPEN" 12 | payload_close: "CLOSE" 13 | retain: false -------------------------------------------------------------------------------- /covers/MasterBedroomBlinds.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Master Bedroom Blinds" 3 | state_topic: "Blinds007/blindsCommand" 4 | command_topic: "blinds/masterbedroom/blindsCommand" 5 | value_template: > 6 | {% if value == "CLOSE" %} 7 | closed 8 | {% else %} 9 | open 10 | {% endif %} 11 | payload_open: "OPEN" 12 | payload_close: "CLOSE" 13 | retain: false -------------------------------------------------------------------------------- /covers/PrimaryGarageDoor.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Primary Garage Door" 3 | state_topic: "shellies/shelly-primary-garage/input/0" 4 | command_topic: "shellies/shelly-primary-garage/relay/0/command" 5 | value_template: > 6 | {% if value == "0" %} 7 | closed 8 | {% else %} 9 | open 10 | {% endif %} 11 | payload_open: "on" 12 | payload_close: "on" 13 | retain: false -------------------------------------------------------------------------------- /covers/SecondaryGarageDoor.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Secondary Garage Door" 3 | state_topic: "shellies/shelly-secondary-garage/input/0" 4 | command_topic: "shellies/shelly-secondary-garage/relay/0/command" 5 | value_template: > 6 | {% if value == "0" %} 7 | closed 8 | {% else %} 9 | open 10 | {% endif %} 11 | payload_open: "on" 12 | payload_close: "on" 13 | retain: false -------------------------------------------------------------------------------- /custom_components/alexa_media/alexa_media.py: -------------------------------------------------------------------------------- 1 | """ 2 | Alexa Devices Base Class. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | 6 | For more details about this platform, please refer to the documentation at 7 | https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 8 | """ 9 | 10 | import logging 11 | from typing import Dict, Text # noqa pylint: disable=unused-import 12 | 13 | from alexapy import AlexaAPI, hide_email 14 | 15 | from .const import DATA_ALEXAMEDIA 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | 20 | class AlexaMedia: 21 | """Implementation of Alexa Media Base object.""" 22 | 23 | def __init__(self, device, login) -> None: 24 | # pylint: disable=unexpected-keyword-arg 25 | """Initialize the Alexa device.""" 26 | 27 | # Class info 28 | self._login = login 29 | self.alexa_api = AlexaAPI(device, login) 30 | self.email = login.email 31 | self.account = hide_email(login.email) 32 | 33 | def check_login_changes(self): 34 | """Update Login object if it has changed.""" 35 | # _LOGGER.debug("Checking if Login object has changed") 36 | try: 37 | login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.email]["login_obj"] 38 | except (AttributeError, KeyError): 39 | return 40 | # _LOGGER.debug("Login object %s closed status: %s", login, login.session.closed) 41 | # _LOGGER.debug( 42 | # "Alexaapi %s closed status: %s", 43 | # self.alexa_api, 44 | # self.alexa_api._session.closed, 45 | # ) 46 | if self.alexa_api.update_login(login): 47 | _LOGGER.debug("Login object has changed; updating") 48 | self._login = login 49 | self.email = login.email 50 | self.account = hide_email(login.email) 51 | -------------------------------------------------------------------------------- /custom_components/alexa_media/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support to interface with Alexa Devices. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | 6 | For more details about this platform, please refer to the documentation at 7 | https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 8 | """ 9 | from datetime import timedelta 10 | 11 | __version__ = "3.10.2" 12 | PROJECT_URL = "https://github.com/custom-components/alexa_media_player/" 13 | ISSUE_URL = f"{PROJECT_URL}issues" 14 | 15 | DOMAIN = "alexa_media" 16 | DATA_ALEXAMEDIA = "alexa_media" 17 | 18 | PLAY_SCAN_INTERVAL = 20 19 | SCAN_INTERVAL = timedelta(seconds=60) 20 | MIN_TIME_BETWEEN_SCANS = SCAN_INTERVAL 21 | MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) 22 | 23 | ALEXA_COMPONENTS = [ 24 | "media_player", 25 | ] 26 | DEPENDENT_ALEXA_COMPONENTS = [ 27 | "notify", 28 | "switch", 29 | "sensor", 30 | "alarm_control_panel", 31 | "light", 32 | ] 33 | 34 | HTTP_COOKIE_HEADER = "# HTTP Cookie File" 35 | CONF_ACCOUNTS = "accounts" 36 | CONF_COOKIES_TXT = "cookies_txt" 37 | CONF_DEBUG = "debug" 38 | CONF_HASS_URL = "hass_url" 39 | CONF_INCLUDE_DEVICES = "include_devices" 40 | CONF_EXCLUDE_DEVICES = "exclude_devices" 41 | CONF_QUEUE_DELAY = "queue_delay" 42 | CONF_EXTENDED_ENTITY_DISCOVERY = "extended_entity_discovery" 43 | CONF_SECURITYCODE = "securitycode" 44 | CONF_OTPSECRET = "otp_secret" 45 | CONF_PROXY = "proxy" 46 | CONF_TOTP_REGISTER = "registered" 47 | CONF_OAUTH = "oauth" 48 | CONF_OAUTH_LOGIN = "oauth_login" 49 | DATA_LISTENER = "listener" 50 | 51 | EXCEPTION_TEMPLATE = "An exception of type {0} occurred. Arguments:\n{1!r}" 52 | 53 | DEFAULT_EXTENDED_ENTITY_DISCOVERY = False 54 | DEFAULT_QUEUE_DELAY = 1.5 55 | SERVICE_CLEAR_HISTORY = "clear_history" 56 | SERVICE_UPDATE_LAST_CALLED = "update_last_called" 57 | SERVICE_FORCE_LOGOUT = "force_logout" 58 | 59 | RECURRING_PATTERN = { 60 | None: "Never Repeat", 61 | "P1D": "Every day", 62 | "XXXX-WE": "Weekends", 63 | "XXXX-WD": "Weekdays", 64 | "XXXX-WXX-1": "Every Monday", 65 | "XXXX-WXX-2": "Every Tuesday", 66 | "XXXX-WXX-3": "Every Wednesday", 67 | "XXXX-WXX-4": "Every Thursday", 68 | "XXXX-WXX-5": "Every Friday", 69 | "XXXX-WXX-6": "Every Saturday", 70 | "XXXX-WXX-7": "Every Sunday", 71 | } 72 | 73 | RECURRING_PATTERN_ISO_SET = { 74 | None: {}, 75 | "P1D": {1, 2, 3, 4, 5, 6, 7}, 76 | "XXXX-WE": {6, 7}, 77 | "XXXX-WD": {1, 2, 3, 4, 5}, 78 | "XXXX-WXX-1": {1}, 79 | "XXXX-WXX-2": {2}, 80 | "XXXX-WXX-3": {3}, 81 | "XXXX-WXX-4": {4}, 82 | "XXXX-WXX-5": {5}, 83 | "XXXX-WXX-6": {6}, 84 | "XXXX-WXX-7": {7}, 85 | } 86 | 87 | ATTR_MESSAGE = "message" 88 | ATTR_EMAIL = "email" 89 | ATTR_NUM_ENTRIES = "entries" 90 | STARTUP = """ 91 | ------------------------------------------------------------------- 92 | {} 93 | Version: {} 94 | This is a custom component 95 | If you have any issues with this you need to open an issue here: 96 | {} 97 | ------------------------------------------------------------------- 98 | """.format( 99 | DOMAIN, __version__, ISSUE_URL 100 | ) 101 | 102 | AUTH_CALLBACK_PATH = "/auth/alexamedia/callback" 103 | AUTH_CALLBACK_NAME = "auth:alexamedia:callback" 104 | AUTH_PROXY_PATH = "/auth/alexamedia/proxy" 105 | AUTH_PROXY_NAME = "auth:alexamedia:proxy" 106 | -------------------------------------------------------------------------------- /custom_components/alexa_media/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "alexa_media", 3 | "name": "Alexa Media Player", 4 | "version": "3.10.2", 5 | "config_flow": true, 6 | "documentation": "https://github.com/custom-components/alexa_media_player/wiki", 7 | "issue_tracker": "https://github.com/custom-components/alexa_media_player/issues", 8 | "dependencies": ["persistent_notification", "http"], 9 | "codeowners": ["@keatontaylor", "@alandtse"], 10 | "requirements": ["alexapy==1.25.1", "packaging~=20.3", "wrapt~=1.12.1"], 11 | "iot_class": "cloud_polling" 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/alexa_media/services.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | update_last_called: 3 | # Description of the service 4 | description: Forces update of last_called echo device for each Alexa account. 5 | # Different fields that your service accepts 6 | fields: 7 | # Key of the field 8 | email: 9 | # Description of the field 10 | description: List of Alexa accounts to update. If empty, will update all known accounts. 11 | # Example value that can be passed for this field 12 | example: "my_email@alexa.com" 13 | 14 | clear_history: 15 | # Description of the service 16 | description: Clear last entries from Alexa history for each Alexa account. 17 | # Different fields that your service accepts 18 | fields: 19 | # Key of the field 20 | email: 21 | # Description of the field 22 | description: List of Alexa accounts to update. If empty, will delete from all known accounts. 23 | # Example value that can be passed for this field 24 | example: "my_email@alexa.com" 25 | entries: 26 | # Description of the field 27 | description: Number of entries to clear from 1 to 50. If empty, clear 50. 28 | # Example value that can be passed for this field 29 | example: 50 30 | 31 | force_logout: 32 | # Description of the service 33 | description: Force logout of Alexa Login account and deletion of .pickle. Intended for debugging use. 34 | # Different fields that your service accepts 35 | fields: 36 | # Key of the field 37 | email: 38 | # Description of the field 39 | description: List of Alexa accounts to log out. If empty, will log out from all known accounts. 40 | # Example value that can be passed for this field 41 | example: "my_email@alexa.com" 42 | -------------------------------------------------------------------------------- /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_repository.py: -------------------------------------------------------------------------------- 1 | """API Handler for hacs_repository""" 2 | import homeassistant.helpers.config_validation as cv 3 | import voluptuous as vol 4 | from aiogithubapi import AIOGitHubAPIException 5 | from homeassistant.components import websocket_api 6 | 7 | from custom_components.hacs.helpers.functions.logger import getLogger 8 | from custom_components.hacs.share import get_hacs 9 | 10 | 11 | @websocket_api.async_response 12 | @websocket_api.websocket_command( 13 | { 14 | vol.Required("type"): "hacs/repository", 15 | vol.Optional("action"): cv.string, 16 | vol.Optional("repository"): cv.string, 17 | } 18 | ) 19 | async def hacs_repository(hass, connection, msg): 20 | """Handle get media player cover command.""" 21 | hacs = get_hacs() 22 | logger = getLogger() 23 | data = {} 24 | repository = None 25 | 26 | repo_id = msg.get("repository") 27 | action = msg.get("action") 28 | if repo_id is None or action is None: 29 | return 30 | 31 | try: 32 | repository = hacs.get_by_id(repo_id) 33 | logger.debug(f"Running {action} for {repository.data.full_name}") 34 | 35 | if action == "update": 36 | await repository.update_repository(ignore_issues=True, force=True) 37 | repository.status.updated_info = True 38 | 39 | elif action == "install": 40 | repository.data.new = False 41 | was_installed = repository.data.installed 42 | await repository.async_install() 43 | if not was_installed: 44 | hass.bus.async_fire("hacs/reload", {"force": True}) 45 | 46 | elif action == "not_new": 47 | repository.data.new = False 48 | 49 | elif action == "uninstall": 50 | repository.data.new = False 51 | await repository.update_repository(True) 52 | await repository.uninstall() 53 | 54 | elif action == "hide": 55 | repository.data.hide = True 56 | 57 | elif action == "unhide": 58 | repository.data.hide = False 59 | 60 | elif action == "show_beta": 61 | repository.data.show_beta = True 62 | await repository.update_repository() 63 | 64 | elif action == "hide_beta": 65 | repository.data.show_beta = False 66 | await repository.update_repository() 67 | 68 | elif action == "toggle_beta": 69 | repository.data.show_beta = not repository.data.show_beta 70 | await repository.update_repository() 71 | 72 | elif action == "delete": 73 | repository.data.show_beta = False 74 | repository.remove() 75 | 76 | elif action == "release_notes": 77 | data = [ 78 | { 79 | "name": x.attributes["name"], 80 | "body": x.attributes["body"], 81 | "tag": x.attributes["tag_name"], 82 | } 83 | for x in repository.releases.objects 84 | ] 85 | 86 | elif action == "set_version": 87 | if msg["version"] == repository.data.default_branch: 88 | repository.data.selected_tag = None 89 | else: 90 | repository.data.selected_tag = msg["version"] 91 | await repository.update_repository() 92 | 93 | hass.bus.async_fire("hacs/reload", {"force": True}) 94 | 95 | else: 96 | logger.error(f"WS action '{action}' is not valid") 97 | 98 | await hacs.data.async_write() 99 | message = None 100 | except AIOGitHubAPIException as exception: 101 | message = exception 102 | except AttributeError as exception: 103 | message = f"Could not use repository with ID {repo_id} ({exception})" 104 | except (Exception, BaseException) as exception: # pylint: disable=broad-except 105 | message = exception 106 | 107 | if message is not None: 108 | logger.error(message) 109 | hass.bus.async_fire("hacs/error", {"message": str(message)}) 110 | 111 | if repository: 112 | repository.state = None 113 | connection.send_message(websocket_api.result_message(msg["id"], data)) 114 | -------------------------------------------------------------------------------- /custom_components/hacs/api/hacs_repository_data.py: -------------------------------------------------------------------------------- 1 | """API Handler for hacs_repository_data""" 2 | import sys 3 | 4 | import homeassistant.helpers.config_validation as cv 5 | import voluptuous as vol 6 | from aiogithubapi import AIOGitHubAPIException 7 | from homeassistant.components import websocket_api 8 | 9 | from custom_components.hacs.helpers.classes.exceptions import HacsException 10 | from custom_components.hacs.helpers.functions.logger import getLogger 11 | from custom_components.hacs.helpers.functions.misc import extract_repository_from_url 12 | from custom_components.hacs.helpers.functions.register_repository import ( 13 | register_repository, 14 | ) 15 | from custom_components.hacs.share import get_hacs 16 | 17 | _LOGGER = getLogger() 18 | 19 | 20 | @websocket_api.async_response 21 | @websocket_api.websocket_command( 22 | { 23 | vol.Required("type"): "hacs/repository/data", 24 | vol.Optional("action"): cv.string, 25 | vol.Optional("repository"): cv.string, 26 | vol.Optional("data"): cv.string, 27 | } 28 | ) 29 | async def hacs_repository_data(hass, connection, msg): 30 | """Handle get media player cover command.""" 31 | hacs = get_hacs() 32 | repo_id = msg.get("repository") 33 | action = msg.get("action") 34 | data = msg.get("data") 35 | 36 | if repo_id is None: 37 | return 38 | 39 | if action == "add": 40 | repo_id = extract_repository_from_url(repo_id) 41 | if repo_id is None: 42 | return 43 | 44 | if repo_id in hacs.common.skip: 45 | hacs.common.skip.remove(repo_id) 46 | 47 | if not hacs.get_by_name(repo_id): 48 | try: 49 | registration = await register_repository(repo_id, data.lower()) 50 | if registration is not None: 51 | raise HacsException(registration) 52 | except ( 53 | Exception, 54 | BaseException, 55 | ) as exception: # pylint: disable=broad-except 56 | hass.bus.async_fire( 57 | "hacs/error", 58 | { 59 | "action": "add_repository", 60 | "exception": str(sys.exc_info()[0].__name__), 61 | "message": str(exception), 62 | }, 63 | ) 64 | else: 65 | hass.bus.async_fire( 66 | "hacs/error", 67 | { 68 | "action": "add_repository", 69 | "message": f"Repository '{repo_id}' exists in the store.", 70 | }, 71 | ) 72 | 73 | repository = hacs.get_by_name(repo_id) 74 | else: 75 | repository = hacs.get_by_id(repo_id) 76 | 77 | if repository is None: 78 | hass.bus.async_fire("hacs/repository", {}) 79 | return 80 | 81 | _LOGGER.debug("Running %s for %s", action, repository.data.full_name) 82 | try: 83 | if action == "set_state": 84 | repository.state = data 85 | 86 | elif action == "set_version": 87 | repository.data.selected_tag = data 88 | await repository.update_repository() 89 | 90 | repository.state = None 91 | 92 | elif action == "install": 93 | was_installed = repository.data.installed 94 | repository.data.selected_tag = data 95 | await repository.update_repository() 96 | await repository.async_install() 97 | repository.state = None 98 | if not was_installed: 99 | hass.bus.async_fire("hacs/reload", {"force": True}) 100 | 101 | elif action == "add": 102 | repository.state = None 103 | 104 | else: 105 | repository.state = None 106 | _LOGGER.error("WS action '%s' is not valid", action) 107 | 108 | message = None 109 | except AIOGitHubAPIException as exception: 110 | message = exception 111 | except AttributeError as exception: 112 | message = f"Could not use repository with ID {repo_id} ({exception})" 113 | except (Exception, BaseException) as exception: # pylint: disable=broad-except 114 | message = exception 115 | 116 | if message is not None: 117 | _LOGGER.error(message) 118 | hass.bus.async_fire("hacs/error", {"message": str(message)}) 119 | 120 | await hacs.data.async_write() 121 | connection.send_message(websocket_api.result_message(msg["id"], {})) 122 | -------------------------------------------------------------------------------- /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/base.py: -------------------------------------------------------------------------------- 1 | """Base HACS class.""" 2 | import logging 3 | from typing import List, Optional, TYPE_CHECKING 4 | import pathlib 5 | 6 | import attr 7 | from aiogithubapi.github import AIOGitHubAPI 8 | from aiogithubapi.objects.repository import AIOGitHubAPIRepository 9 | from homeassistant.core import HomeAssistant 10 | 11 | from .enums import HacsDisabledReason, HacsStage 12 | from .helpers.functions.logger import getLogger 13 | from .models.core import HacsCore 14 | from .models.frontend import HacsFrontend 15 | from .models.system import HacsSystem 16 | 17 | if TYPE_CHECKING: 18 | from .helpers.classes.repository import HacsRepository 19 | 20 | 21 | class HacsCommon: 22 | """Common for HACS.""" 23 | 24 | categories: List = [] 25 | default: List = [] 26 | installed: List = [] 27 | skip: List = [] 28 | 29 | 30 | class HacsStatus: 31 | """HacsStatus.""" 32 | 33 | startup: bool = True 34 | new: bool = False 35 | background_task: bool = False 36 | reloading_data: bool = False 37 | upgrading_all: bool = False 38 | 39 | 40 | @attr.s 41 | class HacsBaseAttributes: 42 | """Base HACS class.""" 43 | 44 | _default: Optional[AIOGitHubAPIRepository] 45 | _github: Optional[AIOGitHubAPI] 46 | _hass: Optional[HomeAssistant] 47 | _repository: Optional[AIOGitHubAPIRepository] 48 | _stage: HacsStage = HacsStage.SETUP 49 | _common: Optional[HacsCommon] 50 | 51 | core: HacsCore = attr.ib(HacsCore) 52 | common: HacsCommon = attr.ib(HacsCommon) 53 | status: HacsStatus = attr.ib(HacsStatus) 54 | frontend: HacsFrontend = attr.ib(HacsFrontend) 55 | log: logging.Logger = getLogger() 56 | system: HacsSystem = attr.ib(HacsSystem) 57 | repositories: List["HacsRepository"] = [] 58 | 59 | 60 | @attr.s 61 | class HacsBase(HacsBaseAttributes): 62 | """Base HACS class.""" 63 | 64 | @property 65 | def stage(self) -> HacsStage: 66 | """Returns a HacsStage object.""" 67 | return self._stage 68 | 69 | @stage.setter 70 | def stage(self, value: HacsStage) -> None: 71 | """Set the value for the stage property.""" 72 | self._stage = value 73 | 74 | @property 75 | def github(self) -> Optional[AIOGitHubAPI]: 76 | """Returns a AIOGitHubAPI object.""" 77 | return self._github 78 | 79 | @github.setter 80 | def github(self, value: AIOGitHubAPI) -> None: 81 | """Set the value for the github property.""" 82 | self._github = value 83 | 84 | @property 85 | def repository(self) -> Optional[AIOGitHubAPIRepository]: 86 | """Returns a AIOGitHubAPIRepository object representing hacs/integration.""" 87 | return self._repository 88 | 89 | @repository.setter 90 | def repository(self, value: AIOGitHubAPIRepository) -> None: 91 | """Set the value for the repository property.""" 92 | self._repository = value 93 | 94 | @property 95 | def default(self) -> Optional[AIOGitHubAPIRepository]: 96 | """Returns a AIOGitHubAPIRepository object representing hacs/default.""" 97 | return self._default 98 | 99 | @default.setter 100 | def default(self, value: AIOGitHubAPIRepository) -> None: 101 | """Set the value for the default property.""" 102 | self._default = value 103 | 104 | @property 105 | def hass(self) -> Optional[HomeAssistant]: 106 | """Returns a HomeAssistant object.""" 107 | return self._hass 108 | 109 | @hass.setter 110 | def hass(self, value: HomeAssistant) -> None: 111 | """Set the value for the default property.""" 112 | self._hass = value 113 | 114 | @property 115 | def integration_dir(self) -> pathlib.Path: 116 | """Return the HACS integration dir.""" 117 | return pathlib.Path(__file__).parent 118 | 119 | def disable(self, reason: HacsDisabledReason) -> None: 120 | """Disable HACS.""" 121 | self.system.disabled = True 122 | self.system.disabled_reason = reason 123 | self.log.error("HACS is disabled - %s", reason) 124 | 125 | def enable(self) -> None: 126 | """Enable HACS.""" 127 | self.system.disabled = False 128 | self.system.disabled_reason = None 129 | self.log.info("HACS is enabled") 130 | -------------------------------------------------------------------------------- /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/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/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/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/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/repositorydata.py: -------------------------------------------------------------------------------- 1 | """Repository data.""" 2 | from datetime import datetime 3 | from typing import List 4 | 5 | import attr 6 | 7 | 8 | @attr.s(auto_attribs=True) 9 | class RepositoryData: 10 | """RepositoryData class.""" 11 | 12 | archived: bool = False 13 | authors: List[str] = [] 14 | category: str = "" 15 | content_in_root: bool = False 16 | country: List[str] = [] 17 | config_flow: bool = False 18 | default_branch: str = None 19 | description: str = "" 20 | domain: str = "" 21 | domains: List[str] = [] 22 | downloads: int = 0 23 | etag_repository: str = None 24 | file_name: str = "" 25 | filename: str = "" 26 | first_install: bool = False 27 | fork: bool = False 28 | full_name: str = "" 29 | hacs: str = None # Minimum HACS version 30 | hide: bool = False 31 | hide_default_branch: bool = False 32 | homeassistant: str = None # Minimum Home Assistant version 33 | id: int = 0 34 | iot_class: str = None 35 | installed: bool = False 36 | installed_commit: str = None 37 | installed_version: str = None 38 | open_issues: int = 0 39 | last_commit: str = None 40 | last_version: str = None 41 | last_updated: str = 0 42 | manifest_name: str = None 43 | new: bool = True 44 | persistent_directory: str = None 45 | pushed_at: str = "" 46 | releases: bool = False 47 | render_readme: bool = False 48 | published_tags: List[str] = [] 49 | selected_tag: str = None 50 | show_beta: bool = False 51 | stargazers_count: int = 0 52 | topics: List[str] = [] 53 | zip_release: bool = False 54 | 55 | @property 56 | def stars(self): 57 | """Return the stargazers count.""" 58 | return self.stargazers_count or 0 59 | 60 | @property 61 | def name(self): 62 | """Return the name.""" 63 | if self.category in ["integration", "netdaemon"]: 64 | return self.domain 65 | return self.full_name.split("/")[-1] 66 | 67 | def to_json(self): 68 | """Export to json.""" 69 | return attr.asdict(self) 70 | 71 | @staticmethod 72 | def create_from_dict(source: dict): 73 | """Set attributes from dicts.""" 74 | data = RepositoryData() 75 | for key in source: 76 | print(key) 77 | if key in data.__dict__: 78 | if key == "pushed_at": 79 | if source[key] == "": 80 | continue 81 | if "Z" in source[key]: 82 | setattr( 83 | data, 84 | key, 85 | datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%SZ"), 86 | ) 87 | else: 88 | setattr( 89 | data, 90 | key, 91 | datetime.strptime(source[key], "%Y-%m-%dT%H:%M:%S"), 92 | ) 93 | elif key == "id": 94 | setattr(data, key, str(source[key])) 95 | elif key == "country": 96 | if isinstance(source[key], str): 97 | setattr(data, key, [source[key]]) 98 | else: 99 | setattr(data, key, source[key]) 100 | else: 101 | setattr(data, key, source[key]) 102 | return data 103 | 104 | def update_data(self, data: dict): 105 | """Update data of the repository.""" 106 | for key in data: 107 | if key in self.__dict__: 108 | if key == "pushed_at": 109 | if data[key] == "": 110 | continue 111 | if "Z" in data[key]: 112 | setattr( 113 | self, 114 | key, 115 | datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%SZ"), 116 | ) 117 | else: 118 | setattr( 119 | self, key, datetime.strptime(data[key], "%Y-%m-%dT%H:%M:%S") 120 | ) 121 | elif key == "id": 122 | setattr(self, key, str(data[key])) 123 | elif key == "country": 124 | if isinstance(data[key], str): 125 | setattr(self, key, [data[key]]) 126 | else: 127 | setattr(self, key, data[key]) 128 | else: 129 | setattr(self, key, data[key]) 130 | -------------------------------------------------------------------------------- /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/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/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/validate_repository.py: -------------------------------------------------------------------------------- 1 | """Helper to do common validation for repositories.""" 2 | from aiogithubapi import AIOGitHubAPIException 3 | 4 | from custom_components.hacs.helpers.classes.exceptions import ( 5 | HacsException, 6 | HacsNotModifiedException, 7 | HacsRepositoryArchivedException, 8 | ) 9 | from custom_components.hacs.helpers.functions.information import ( 10 | get_releases, 11 | get_repository, 12 | get_tree, 13 | ) 14 | from custom_components.hacs.helpers.functions.version_to_install import ( 15 | version_to_install, 16 | ) 17 | from custom_components.hacs.share import get_hacs, is_removed 18 | 19 | 20 | async def common_validate(repository, ignore_issues=False): 21 | """Common validation steps of the repository.""" 22 | repository.validate.errors = [] 23 | 24 | # Make sure the repository exist. 25 | repository.logger.debug("%s Checking repository.", repository) 26 | await common_update_data(repository, ignore_issues) 27 | 28 | # Step 6: Get the content of hacs.json 29 | await repository.get_repository_manifest_content() 30 | 31 | 32 | async def common_update_data(repository, ignore_issues=False, force=False): 33 | """Common update data.""" 34 | hacs = get_hacs() 35 | releases = [] 36 | try: 37 | repository_object, etag = await get_repository( 38 | hacs.session, 39 | hacs.configuration.token, 40 | repository.data.full_name, 41 | etag=None 42 | if force or repository.data.installed 43 | else repository.data.etag_repository, 44 | ) 45 | repository.repository_object = repository_object 46 | repository.data.update_data(repository_object.attributes) 47 | repository.data.etag_repository = etag 48 | except HacsNotModifiedException: 49 | return 50 | except (AIOGitHubAPIException, HacsException) as exception: 51 | if not hacs.status.startup: 52 | repository.logger.error("%s %s", repository, exception) 53 | if not ignore_issues: 54 | repository.validate.errors.append("Repository does not exist.") 55 | raise HacsException(exception) from None 56 | 57 | # Make sure the repository is not archived. 58 | if repository.data.archived and not ignore_issues: 59 | repository.validate.errors.append("Repository is archived.") 60 | raise HacsRepositoryArchivedException("Repository is archived.") 61 | 62 | # Make sure the repository is not in the blacklist. 63 | if is_removed(repository.data.full_name) and not ignore_issues: 64 | repository.validate.errors.append("Repository is in the blacklist.") 65 | raise HacsException("Repository is in the blacklist.") 66 | 67 | # Get releases. 68 | try: 69 | releases = await get_releases( 70 | repository.repository_object, 71 | repository.data.show_beta, 72 | hacs.configuration.release_limit, 73 | ) 74 | if releases: 75 | repository.data.releases = True 76 | repository.releases.objects = [x for x in releases if not x.draft] 77 | repository.data.published_tags = [ 78 | x.tag_name for x in repository.releases.objects 79 | ] 80 | repository.data.last_version = next(iter(repository.data.published_tags)) 81 | 82 | except (AIOGitHubAPIException, HacsException): 83 | repository.data.releases = False 84 | 85 | if not repository.force_branch: 86 | repository.ref = version_to_install(repository) 87 | if repository.data.releases: 88 | for release in repository.releases.objects or []: 89 | if release.tag_name == repository.ref: 90 | assets = release.assets 91 | if assets: 92 | downloads = next(iter(assets)).attributes.get("download_count") 93 | repository.data.downloads = downloads 94 | 95 | repository.logger.debug( 96 | "%s Running checks against %s", repository, repository.ref.replace("tags/", "") 97 | ) 98 | 99 | try: 100 | repository.tree = await get_tree(repository.repository_object, repository.ref) 101 | if not repository.tree: 102 | raise HacsException("No files in tree") 103 | repository.treefiles = [] 104 | for treefile in repository.tree: 105 | repository.treefiles.append(treefile.full_path) 106 | except (AIOGitHubAPIException, HacsException) as exception: 107 | if not hacs.status.startup: 108 | repository.logger.error("%s %s", repository, exception) 109 | if not ignore_issues: 110 | raise HacsException(exception) from None 111 | -------------------------------------------------------------------------------- /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/installation.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,no-member 2 | import os 3 | import tempfile 4 | from abc import ABC 5 | 6 | from custom_components.hacs.helpers.classes.exceptions import HacsException 7 | from custom_components.hacs.helpers.functions.download import download_content 8 | from custom_components.hacs.helpers.functions.version_to_install import ( 9 | version_to_install, 10 | ) 11 | from custom_components.hacs.operational.backup import Backup, BackupNetDaemon 12 | from custom_components.hacs.share import get_hacs 13 | 14 | 15 | class RepositoryMethodPreInstall(ABC): 16 | async def async_pre_install(self) -> None: 17 | pass 18 | 19 | async def _async_pre_install(self) -> None: 20 | self.logger.info("Running pre installation steps") 21 | await self.async_pre_install() 22 | self.logger.info("Pre installation steps completed") 23 | 24 | 25 | class RepositoryMethodInstall(ABC): 26 | async def async_install(self) -> None: 27 | await self._async_pre_install() 28 | self.logger.info("Running installation steps") 29 | await async_install_repository(self) 30 | self.logger.info("Installation steps completed") 31 | await self._async_post_install() 32 | 33 | 34 | class RepositoryMethodPostInstall(ABC): 35 | async def async_post_installation(self) -> None: 36 | pass 37 | 38 | async def _async_post_install(self) -> None: 39 | self.logger.info("Running post installation steps") 40 | await self.async_post_installation() 41 | self.data.new = False 42 | self.hacs.hass.bus.async_fire( 43 | "hacs/repository", 44 | {"id": 1337, "action": "install", "repository": self.data.full_name}, 45 | ) 46 | self.logger.info("Post installation steps completed") 47 | 48 | 49 | async def async_install_repository(repository): 50 | """Common installation steps of the repository.""" 51 | hacs = get_hacs() 52 | persistent_directory = None 53 | await repository.update_repository() 54 | if repository.content.path.local is None: 55 | raise HacsException("repository.content.path.local is None") 56 | repository.validate.errors = [] 57 | 58 | if not repository.can_install: 59 | raise HacsException( 60 | "The version of Home Assistant is not compatible with this version" 61 | ) 62 | 63 | version = version_to_install(repository) 64 | if version == repository.data.default_branch: 65 | repository.ref = version 66 | else: 67 | repository.ref = f"tags/{version}" 68 | 69 | if repository.data.installed and repository.data.category == "netdaemon": 70 | persistent_directory = await hacs.hass.async_add_executor_job( 71 | BackupNetDaemon, repository 72 | ) 73 | await hacs.hass.async_add_executor_job(persistent_directory.create) 74 | 75 | elif repository.data.persistent_directory: 76 | if os.path.exists( 77 | f"{repository.content.path.local}/{repository.data.persistent_directory}" 78 | ): 79 | persistent_directory = Backup( 80 | f"{repository.content.path.local}/{repository.data.persistent_directory}", 81 | tempfile.gettempdir() + "/hacs_persistent_directory/", 82 | ) 83 | await hacs.hass.async_add_executor_job(persistent_directory.create) 84 | 85 | if repository.data.installed and not repository.content.single: 86 | backup = Backup(repository.content.path.local) 87 | await hacs.hass.async_add_executor_job(backup.create) 88 | 89 | if repository.data.zip_release and version != repository.data.default_branch: 90 | await repository.download_zip_files(repository) 91 | else: 92 | await download_content(repository) 93 | 94 | if repository.validate.errors: 95 | for error in repository.validate.errors: 96 | repository.logger.error(error) 97 | if repository.data.installed and not repository.content.single: 98 | await hacs.hass.async_add_executor_job(backup.restore) 99 | 100 | if repository.data.installed and not repository.content.single: 101 | await hacs.hass.async_add_executor_job(backup.cleanup) 102 | 103 | if persistent_directory is not None: 104 | await hacs.hass.async_add_executor_job(persistent_directory.restore) 105 | await hacs.hass.async_add_executor_job(persistent_directory.cleanup) 106 | 107 | if repository.validate.success: 108 | if repository.data.full_name not in repository.hacs.common.installed: 109 | if repository.data.full_name == "hacs/integration": 110 | repository.hacs.common.installed.append(repository.data.full_name) 111 | repository.data.installed = True 112 | repository.data.installed_commit = repository.data.last_commit 113 | 114 | if version == repository.data.default_branch: 115 | repository.data.installed_version = None 116 | else: 117 | repository.data.installed_version = version 118 | -------------------------------------------------------------------------------- /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/iconset.js: -------------------------------------------------------------------------------- 1 | window.customIconsets = window.customIconsets || {}; 2 | window.customIconsets["hacs"] = async () => { 3 | return { 4 | path: 5 | "m 20.064849,22.306912 c -0.0319,0.369835 -0.280561,0.707789 -0.656773,0.918212 -0.280572,0.153036 -0.605773,0.229553 -0.950094,0.229553 -0.0765,0 -0.146661,-0.0064 -0.216801,-0.01275 -0.605774,-0.05739 -1.135016,-0.344329 -1.402827,-0.7588 l 0.784304,-0.516495 c 0.0893,0.146659 0.344331,0.312448 0.707793,0.34433 0.235931,0.02551 0.471852,-0.01913 0.637643,-0.108401 0.101998,-0.05101 0.172171,-0.127529 0.17854,-0.191295 0.0065,-0.08289 -0.0255,-0.369835 -0.733293,-0.439975 -1.013854,-0.09565 -1.645127,-0.688661 -1.568606,-1.460214 0.0319,-0.382589 0.280561,-0.714165 0.663153,-0.930965 0.331571,-0.172165 0.752423,-0.25506 1.166895,-0.210424 0.599382,0.05739 1.128635,0.344329 1.402816,0.7588 l -0.784304,0.510118 c -0.0893,-0.140282 -0.344331,-0.299694 -0.707782,-0.331576 -0.235932,-0.02551 -0.471863,0.01913 -0.637654,0.10202 -0.0956,0.05739 -0.165791,0.133906 -0.17216,0.191295 -0.0255,0.293317 0.465482,0.420847 0.726913,0.439976 v 0.0064 c 1.020234,0.09565 1.638757,0.66953 1.562237,1.460213 z m -7.466854,-0.988354 c 0,-1.192401 0.962855,-2.155249 2.15525,-2.155249 0.599393,0 1.179645,0.25506 1.594117,0.707789 l -0.695033,0.624895 c -0.235931,-0.25506 -0.561133,-0.401718 -0.899084,-0.401718 -0.675903,0 -1.217906,0.542 -1.217906,1.217906 0,0.66953 0.542003,1.217908 1.217906,1.217908 0.337951,0 0.663153,-0.140283 0.899084,-0.401718 l 0.695033,0.631271 c -0.414472,0.452729 -0.988355,0.707788 -1.594117,0.707788 -1.192395,0 -2.15525,-0.969224 -2.15525,-2.148872 z M 8.6573365,23.461054 10.353474,19.14418 h 0.624893 l 1.568618,4.316874 H 11.52037 L 11.265308,22.734136 H 9.964513 l -0.274192,0.726918 z m 1.6833885,-1.68339 h 0.580263 L 10.646796,21.012487 Z M 8.1089536,19.156932 v 4.297745 H 7.1461095 v -1.645131 h -1.606867 v 1.645131 H 4.5763876 v -4.297745 h 0.9628549 v 1.696143 h 1.606867 V 19.156932 Z M 20.115859,4.2997436 C 20.090359,4.159461 19.969198,4.0574375 19.822548,4.0574375 H 14.141102 10.506516 4.8250686 c -0.14665,0 -0.2678112,0.1020202 -0.2933108,0.2423061 L 3.690064,8.8461703 c -0.00651,0.01913 -0.00651,0.03826 -0.00651,0.057391 v 1.5239797 c 0,0.165789 0.133911,0.299694 0.2996911,0.299694 H 4.5762579 20.0711 20.664112 c 0.165781,0 0.299691,-0.133905 0.299691,-0.299694 V 8.8971848 c 0,-0.01913 0,-0.03826 -0.0065,-0.05739 z M 4.5763876,17.358767 c 0,0.184917 0.1466608,0.331577 0.3315819,0.331577 h 5.5985465 3.634586 0.924594 c 0.184911,0 0.331571,-0.14666 0.331571,-0.331577 v -4.744098 c 0,-0.184918 0.146661,-0.331577 0.331582,-0.331577 h 2.894913 c 0.184921,0 0.331582,0.146659 0.331582,0.331577 v 4.744098 c 0,0.184917 0.146661,0.331577 0.331571,0.331577 h 0.446363 c 0.18491,0 0.331571,-0.14666 0.331571,-0.331577 v -5.636804 c 0,-0.184918 -0.146661,-0.331577 -0.331571,-0.331577 H 4.9079695 c -0.1849211,0 -0.3315819,0.146659 -0.3315819,0.331577 z m 1.6578879,-4.852498 h 5.6495565 c 0.15303,0 0.280561,0.12753 0.280561,0.280564 v 3.513438 c 0,0.153036 -0.127531,0.280566 -0.280561,0.280566 H 6.2342755 c -0.1530412,0 -0.2805719,-0.12753 -0.2805719,-0.280566 v -3.513438 c 0,-0.159411 0.1275307,-0.280564 0.2805719,-0.280564 z M 19.790657,3.3879075 H 4.8569594 c -0.1530412,0 -0.2805718,-0.1275296 -0.2805718,-0.2805642 V 1.3665653 C 4.5763876,1.2135296 4.7039182,1.086 4.8569594,1.086 H 19.790657 c 0.153041,0 0.280572,0.1275296 0.280572,0.2805653 v 1.740778 c 0,0.1530346 -0.127531,0.2805642 -0.280572,0.2805642 z", 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /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 | "iot_class": "cloud_polling", 16 | "issue_tracker": "https://github.com/hacs/integration/issues", 17 | "name": "HACS", 18 | "requirements": [ 19 | "aiofiles>=0.6.0", 20 | "aiogithubapi>=21.4.0", 21 | "awesomeversion>=21.2.2", 22 | "backoff>=1.10.0", 23 | "hacs_frontend==20210429001005", 24 | "queueman==0.5" 25 | ], 26 | "version": "1.12.3" 27 | } -------------------------------------------------------------------------------- /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/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/custom_components/hacs/operational/__init__.py -------------------------------------------------------------------------------- /custom_components/hacs/operational/backup.py: -------------------------------------------------------------------------------- 1 | """Backup.""" 2 | import os 3 | import shutil 4 | import tempfile 5 | from time import sleep 6 | 7 | from custom_components.hacs.helpers.functions.is_safe_to_remove import is_safe_to_remove 8 | from custom_components.hacs.helpers.functions.logger import getLogger 9 | 10 | BACKUP_PATH = tempfile.gettempdir() + "/hacs_backup/" 11 | 12 | _LOGGER = getLogger() 13 | 14 | 15 | class Backup: 16 | """Backup.""" 17 | 18 | def __init__(self, local_path, backup_path=BACKUP_PATH): 19 | """initialize.""" 20 | self.local_path = local_path 21 | self.backup_path = backup_path 22 | self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}" 23 | 24 | def create(self): 25 | """Create a backup in /tmp""" 26 | if not os.path.exists(self.local_path): 27 | return 28 | if not is_safe_to_remove(self.local_path): 29 | return 30 | if os.path.exists(self.backup_path): 31 | shutil.rmtree(self.backup_path) 32 | while os.path.exists(self.backup_path): 33 | sleep(0.1) 34 | os.makedirs(self.backup_path, exist_ok=True) 35 | 36 | try: 37 | if os.path.isfile(self.local_path): 38 | shutil.copyfile(self.local_path, self.backup_path_full) 39 | os.remove(self.local_path) 40 | else: 41 | shutil.copytree(self.local_path, self.backup_path_full) 42 | shutil.rmtree(self.local_path) 43 | while os.path.exists(self.local_path): 44 | sleep(0.1) 45 | _LOGGER.debug( 46 | "Backup for %s, created in %s", 47 | self.local_path, 48 | self.backup_path_full, 49 | ) 50 | except (Exception, BaseException): # pylint: disable=broad-except 51 | pass 52 | 53 | def restore(self): 54 | """Restore from backup.""" 55 | if not os.path.exists(self.backup_path_full): 56 | return 57 | 58 | if os.path.isfile(self.backup_path_full): 59 | if os.path.exists(self.local_path): 60 | os.remove(self.local_path) 61 | shutil.copyfile(self.backup_path_full, self.local_path) 62 | else: 63 | if os.path.exists(self.local_path): 64 | shutil.rmtree(self.local_path) 65 | while os.path.exists(self.local_path): 66 | sleep(0.1) 67 | shutil.copytree(self.backup_path_full, self.local_path) 68 | _LOGGER.debug( 69 | "Restored %s, from backup %s", self.local_path, self.backup_path_full 70 | ) 71 | 72 | def cleanup(self): 73 | """Cleanup backup files.""" 74 | if os.path.exists(self.backup_path): 75 | shutil.rmtree(self.backup_path) 76 | while os.path.exists(self.backup_path): 77 | sleep(0.1) 78 | _LOGGER.debug("Backup dir %s cleared", self.backup_path) 79 | 80 | 81 | class BackupNetDaemon: 82 | """BackupNetDaemon.""" 83 | 84 | def __init__(self, repository): 85 | """Initialize.""" 86 | self.repository = repository 87 | self.backup_path = ( 88 | tempfile.gettempdir() + "/hacs_persistent_netdaemon/" + repository.data.name 89 | ) 90 | 91 | def create(self): 92 | """Create a backup in /tmp""" 93 | if not is_safe_to_remove(self.repository.content.path.local): 94 | return 95 | if os.path.exists(self.backup_path): 96 | shutil.rmtree(self.backup_path) 97 | while os.path.exists(self.backup_path): 98 | sleep(0.1) 99 | os.makedirs(self.backup_path, exist_ok=True) 100 | 101 | for filename in os.listdir(self.repository.content.path.local): 102 | if filename.endswith(".yaml"): 103 | source_file_name = f"{self.repository.content.path.local}/{filename}" 104 | target_file_name = f"{self.backup_path}/{filename}" 105 | shutil.copyfile(source_file_name, target_file_name) 106 | 107 | def restore(self): 108 | """Create a backup in /tmp""" 109 | if os.path.exists(self.backup_path): 110 | for filename in os.listdir(self.backup_path): 111 | if filename.endswith(".yaml"): 112 | source_file_name = f"{self.backup_path}/{filename}" 113 | target_file_name = ( 114 | f"{self.repository.content.path.local}/{filename}" 115 | ) 116 | shutil.copyfile(source_file_name, target_file_name) 117 | 118 | def cleanup(self): 119 | """Create a backup in /tmp""" 120 | if os.path.exists(self.backup_path): 121 | shutil.rmtree(self.backup_path) 122 | while os.path.exists(self.backup_path): 123 | sleep(0.1) 124 | _LOGGER.debug("Backup dir %s cleared", self.backup_path) 125 | -------------------------------------------------------------------------------- /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/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/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/integration.py: -------------------------------------------------------------------------------- 1 | """Class for integrations in HACS.""" 2 | from homeassistant.loader import async_get_custom_components 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 | from custom_components.hacs.helpers.functions.filters import ( 8 | get_first_directory_in_directory, 9 | ) 10 | from custom_components.hacs.helpers.functions.information import ( 11 | get_integration_manifest, 12 | ) 13 | from custom_components.hacs.helpers.functions.logger import getLogger 14 | 15 | 16 | class HacsIntegration(HacsRepository): 17 | """Integrations in HACS.""" 18 | 19 | def __init__(self, full_name): 20 | """Initialize.""" 21 | super().__init__() 22 | self.data.full_name = full_name 23 | self.data.full_name_lower = full_name.lower() 24 | self.data.category = HacsCategory.INTEGRATION 25 | self.content.path.remote = "custom_components" 26 | self.content.path.local = self.localpath 27 | 28 | @property 29 | def localpath(self): 30 | """Return localpath.""" 31 | return f"{self.hacs.core.config_path}/custom_components/{self.data.domain}" 32 | 33 | async def async_post_installation(self): 34 | """Run post installation steps.""" 35 | if self.data.config_flow: 36 | if self.data.full_name != "hacs/integration": 37 | await self.reload_custom_components() 38 | if self.data.first_install: 39 | self.pending_restart = False 40 | return 41 | self.pending_restart = True 42 | 43 | async def validate_repository(self): 44 | """Validate.""" 45 | await self.common_validate() 46 | 47 | # Custom step 1: Validate content. 48 | if self.data.content_in_root: 49 | self.content.path.remote = "" 50 | 51 | if self.content.path.remote == "custom_components": 52 | name = get_first_directory_in_directory(self.tree, "custom_components") 53 | if name is None: 54 | raise HacsException( 55 | f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" 56 | ) 57 | self.content.path.remote = f"custom_components/{name}" 58 | 59 | try: 60 | await get_integration_manifest(self) 61 | except HacsException as exception: 62 | if self.hacs.system.action: 63 | raise HacsException(f"::error:: {exception}") from exception 64 | self.logger.error("%s %s", self, exception) 65 | 66 | # Handle potential errors 67 | if self.validate.errors: 68 | for error in self.validate.errors: 69 | if not self.hacs.status.startup: 70 | self.logger.error("%s %s", self, error) 71 | return self.validate.success 72 | 73 | async def update_repository(self, ignore_issues=False, force=False): 74 | """Update.""" 75 | if not await self.common_update(ignore_issues, force): 76 | return 77 | 78 | if self.data.content_in_root: 79 | self.content.path.remote = "" 80 | 81 | if self.content.path.remote == "custom_components": 82 | name = get_first_directory_in_directory(self.tree, "custom_components") 83 | self.content.path.remote = f"custom_components/{name}" 84 | 85 | try: 86 | await get_integration_manifest(self) 87 | except HacsException as exception: 88 | self.logger.error("%s %s", self, exception) 89 | 90 | # Set local path 91 | self.content.path.local = self.localpath 92 | 93 | async def reload_custom_components(self): 94 | """Reload custom_components (and config flows)in HA.""" 95 | self.logger.info("Reloading custom_component cache") 96 | del self.hacs.hass.data["custom_components"] 97 | await async_get_custom_components(self.hacs.hass) 98 | self.logger.info("Custom_component cache reloaded") 99 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/netdaemon.py: -------------------------------------------------------------------------------- 1 | """Class for netdaemon apps 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.filters import ( 6 | get_first_directory_in_directory, 7 | ) 8 | from custom_components.hacs.helpers.functions.logger import getLogger 9 | 10 | 11 | class HacsNetdaemon(HacsRepository): 12 | """Netdaemon apps in HACS.""" 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.NETDAEMON 20 | self.content.path.local = self.localpath 21 | self.content.path.remote = "apps" 22 | 23 | @property 24 | def localpath(self): 25 | """Return localpath.""" 26 | return f"{self.hacs.core.config_path}/netdaemon/apps/{self.data.name}" 27 | 28 | async def validate_repository(self): 29 | """Validate.""" 30 | await self.common_validate() 31 | 32 | # Custom step 1: Validate content. 33 | if self.repository_manifest: 34 | if self.data.content_in_root: 35 | self.content.path.remote = "" 36 | 37 | if self.content.path.remote == "apps": 38 | self.data.domain = get_first_directory_in_directory( 39 | self.tree, self.content.path.remote 40 | ) 41 | self.content.path.remote = f"apps/{self.data.name}" 42 | 43 | compliant = False 44 | for treefile in self.treefiles: 45 | if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( 46 | ".cs" 47 | ): 48 | compliant = True 49 | break 50 | if not compliant: 51 | raise HacsException( 52 | f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" 53 | ) 54 | 55 | # Handle potential errors 56 | if self.validate.errors: 57 | for error in self.validate.errors: 58 | if not self.hacs.status.startup: 59 | self.logger.error("%s %s", self, error) 60 | return self.validate.success 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 appdaemon objects. 68 | if self.repository_manifest: 69 | if self.data.content_in_root: 70 | self.content.path.remote = "" 71 | 72 | if self.content.path.remote == "apps": 73 | self.data.domain = get_first_directory_in_directory( 74 | self.tree, self.content.path.remote 75 | ) 76 | self.content.path.remote = f"apps/{self.data.name}" 77 | 78 | # Set local path 79 | self.content.path.local = self.localpath 80 | 81 | async def async_post_installation(self): 82 | """Run post installation steps.""" 83 | try: 84 | await self.hacs.hass.services.async_call( 85 | "hassio", "addon_restart", {"addon": "c6a2317c_netdaemon"} 86 | ) 87 | except (Exception, BaseException): # pylint: disable=broad-except 88 | pass 89 | -------------------------------------------------------------------------------- /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/sensor.py: -------------------------------------------------------------------------------- 1 | """Sensor platform for HACS.""" 2 | from homeassistant.core import callback 3 | from homeassistant.helpers.entity import Entity 4 | 5 | from custom_components.hacs.const import DOMAIN, INTEGRATION_VERSION, NAME_SHORT 6 | from custom_components.hacs.share import get_hacs 7 | 8 | 9 | async def async_setup_platform( 10 | _hass, _config, async_add_entities, _discovery_info=None 11 | ): 12 | """Setup sensor platform.""" 13 | async_add_entities([HACSSensor()]) 14 | 15 | 16 | async def async_setup_entry(_hass, _config_entry, async_add_devices): 17 | """Setup sensor platform.""" 18 | async_add_devices([HACSSensor()]) 19 | 20 | 21 | class HACSDevice(Entity): 22 | """HACS Device class.""" 23 | 24 | @property 25 | def device_info(self): 26 | """Return device information about HACS.""" 27 | return { 28 | "identifiers": {(DOMAIN, self.unique_id)}, 29 | "name": NAME_SHORT, 30 | "manufacturer": "hacs.xyz", 31 | "model": "", 32 | "sw_version": INTEGRATION_VERSION, 33 | "entry_type": "service", 34 | } 35 | 36 | 37 | class HACSSensor(HACSDevice): 38 | """HACS Sensor class.""" 39 | 40 | def __init__(self): 41 | """Initialize.""" 42 | self._state = None 43 | self.repositories = [] 44 | 45 | @property 46 | def should_poll(self): 47 | """No polling needed.""" 48 | return False 49 | 50 | async def async_update(self): 51 | """Manual updates of the sensor.""" 52 | self._update() 53 | 54 | @callback 55 | def _update_and_write_state(self, *_): 56 | """Update the sensor and write state.""" 57 | self._update() 58 | self.async_write_ha_state() 59 | 60 | @callback 61 | def _update(self): 62 | """Update the sensor.""" 63 | hacs = get_hacs() 64 | if hacs.status.background_task: 65 | return 66 | 67 | self.repositories = [] 68 | 69 | for repository in hacs.repositories: 70 | if ( 71 | repository.pending_upgrade 72 | and repository.data.category in hacs.common.categories 73 | ): 74 | self.repositories.append(repository) 75 | self._state = len(self.repositories) 76 | 77 | @property 78 | def unique_id(self): 79 | """Return a unique ID to use for this sensor.""" 80 | return ( 81 | "0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd" 82 | ) 83 | 84 | @property 85 | def name(self): 86 | """Return the name of the sensor.""" 87 | return "hacs" 88 | 89 | @property 90 | def state(self): 91 | """Return the state of the sensor.""" 92 | return self._state 93 | 94 | @property 95 | def icon(self): 96 | """Return the icon of the sensor.""" 97 | return "hacs:hacs" 98 | 99 | @property 100 | def unit_of_measurement(self): 101 | """Return the unit of measurement.""" 102 | return "pending update(s)" 103 | 104 | @property 105 | def device_state_attributes(self): 106 | """Return attributes for the sensor.""" 107 | repositories = [] 108 | for repository in self.repositories: 109 | repositories.append( 110 | { 111 | "name": repository.data.full_name, 112 | "display_name": repository.display_name, 113 | "installed_version": repository.display_installed_version, 114 | "available_version": repository.display_available_version, 115 | } 116 | ) 117 | return {"repositories": repositories} 118 | 119 | async def async_added_to_hass(self) -> None: 120 | """Register for status events.""" 121 | self.async_on_remove( 122 | self.hass.bus.async_listen("hacs/status", self._update_and_write_state) 123 | ) 124 | -------------------------------------------------------------------------------- /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/shelly/block.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shelly block. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/shelly/ 6 | """ 7 | 8 | from homeassistant.helpers.restore_state import RestoreEntity 9 | from homeassistant.util import slugify #, dt as dt_util 10 | from homeassistant.const import CONF_NAME 11 | 12 | from .const import (CONF_OBJECT_ID_PREFIX, CONF_ENTITY_ID, 13 | CONF_SHOW_ID_IN_NAME, DOMAIN) 14 | 15 | class ShellyBlock(RestoreEntity): 16 | """Base class for Shelly entities""" 17 | 18 | def __init__(self, block, instance, prefix=""): 19 | conf = instance.conf 20 | id_prefix = conf.get(CONF_OBJECT_ID_PREFIX) 21 | self._unique_id = slugify(id_prefix + "_" + block.type + "_" + 22 | block.id + prefix) 23 | self.entity_id = "." + self._unique_id 24 | entity_id = \ 25 | instance._get_specific_config(CONF_ENTITY_ID, None, block.id) 26 | if entity_id is not None: 27 | self.entity_id = "." + slugify(id_prefix + "_" + entity_id + prefix) 28 | self._unique_id += "_" + slugify(entity_id) 29 | self._show_id_in_name = conf.get(CONF_SHOW_ID_IN_NAME) 30 | self._block = block 31 | self.hass = instance.hass 32 | self.instance = instance 33 | self._block.cb_updated.append(self._updated) 34 | block.shelly_device = self #todo, should be array?? 35 | self._name = instance._get_specific_config(CONF_NAME, None, block.id) 36 | self._name_ext = None 37 | self._is_removed = False 38 | self.async_on_remove(self._remove_handler) 39 | self._master_unit = False 40 | self._settings = instance.get_settings(block.id) 41 | 42 | def _remove_handler(self): 43 | self._is_removed = True 44 | self._block.cb_updated.remove(self._updated) 45 | 46 | @property 47 | def name(self): 48 | """Return the display name of this device.""" 49 | if self._name is None: 50 | name = self._block.friendly_name() 51 | else: 52 | name = self._name 53 | if self._name_ext: 54 | name += ' - ' + self._name_ext 55 | if self._show_id_in_name: 56 | name += " [" + self._block.id + "]" 57 | return name 58 | 59 | def _update_ha_state(self): 60 | self.schedule_update_ha_state(True) 61 | 62 | def _updated(self, _block): 63 | """Receive events when the switch state changed (by mobile, 64 | switch etc)""" 65 | disabled = self.registry_entry and self.registry_entry.disabled_by 66 | if self.entity_id is not None and not self._is_removed \ 67 | and not disabled: 68 | self._update_ha_state() 69 | 70 | @property 71 | def device_state_attributes(self): 72 | """Show state attributes in HASS""" 73 | attrs = {'shelly_type': self._block.type_name(), 74 | 'shelly_id': self._block.id, 75 | 'ip_address': self._block.ip_addr 76 | } 77 | 78 | room = self._block.room_name() 79 | if room: 80 | attrs['room'] = room 81 | 82 | if self._master_unit: 83 | attrs['protocols'] = self._block.protocols 84 | if self._block.info_values is not None: 85 | for key, value in self._block.info_values.items(): 86 | if self.instance.conf_attribute(key): 87 | attrs[key] = value 88 | src = '' 89 | 90 | return attrs 91 | 92 | @property 93 | def device_info(self): 94 | return { 95 | 'identifiers': { 96 | (DOMAIN, self._block.unit_id) 97 | }, 98 | 'name': self._block.friendly_name(), 99 | 'manufacturer': 'Allterco', 100 | 'model': self._block.type_name(), 101 | 'sw_version': self._block.fw_version() 102 | } 103 | 104 | @property 105 | def unique_id(self): 106 | """Return the ID of this device.""" 107 | return self._unique_id 108 | 109 | def remove(self): 110 | self._is_removed = True 111 | self.hass.add_job(self.async_remove) 112 | 113 | @property 114 | def available(self): 115 | """Return true if switch is available.""" 116 | return self._block.available() 117 | -------------------------------------------------------------------------------- /custom_components/shelly/configuration_schema.py: -------------------------------------------------------------------------------- 1 | """Shelly Configuration Schemas.""" 2 | # pylint: disable=dangerous-default-value 3 | from homeassistant.const import ( 4 | CONF_DEVICES, CONF_DISCOVERY, CONF_ID, CONF_NAME, CONF_PASSWORD, 5 | CONF_SCAN_INTERVAL, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) 6 | import voluptuous as vol 7 | import homeassistant.helpers.config_validation as cv 8 | from .const import * 9 | 10 | ALL_SENSORS_W_EXTRA = list(ALL_SENSORS.keys()) + list(EXTRA_SENSORS.keys()) 11 | 12 | SENSOR_SCHEMA = vol.Schema({ 13 | vol.Optional(CONF_NAME): cv.string, 14 | }) 15 | 16 | SETTING_SCHEMA = vol.Schema({ 17 | vol.Optional(CONF_DECIMALS): cv.positive_int, 18 | vol.Optional(CONF_DIV): cv.positive_int, 19 | vol.Optional(CONF_UNIT): cv.string 20 | }) 21 | 22 | SETTINGS_SCHEMA = vol.Schema({ 23 | vol.Optional('temperature'): SETTING_SCHEMA, 24 | vol.Optional('humidity'): SETTING_SCHEMA, 25 | vol.Optional('illuminance'): SETTING_SCHEMA, 26 | vol.Optional('current'): SETTING_SCHEMA, 27 | vol.Optional('total_consumption'): SETTING_SCHEMA, 28 | vol.Optional('total_returned'): SETTING_SCHEMA, 29 | vol.Optional('current_consumption'): SETTING_SCHEMA, 30 | vol.Optional('device_temp'): SETTING_SCHEMA, 31 | vol.Optional('voltage'): SETTING_SCHEMA, 32 | vol.Optional('power_factor'): SETTING_SCHEMA, 33 | vol.Optional('uptime'): SETTING_SCHEMA, 34 | vol.Optional('rssi'): SETTING_SCHEMA, 35 | vol.Optional('rssi_level'): SETTING_SCHEMA 36 | }) 37 | 38 | DEVICE_SCHEMA = vol.Schema({ 39 | vol.Required(CONF_ID): cv.string, 40 | vol.Optional(CONF_NAME): cv.string, 41 | vol.Optional(CONF_LIGHT_SWITCH, default=False): cv.boolean, 42 | vol.Optional(CONF_SENSORS): 43 | vol.All(cv.ensure_list, [vol.In(ALL_SENSORS_W_EXTRA)]), 44 | vol.Optional(CONF_UPGRADE_SWITCH): cv.boolean, 45 | vol.Optional(CONF_UPGRADE_BETA_SWITCH): cv.boolean, 46 | vol.Optional(CONF_UNAVALABLE_AFTER_SEC) : cv.positive_int, 47 | vol.Optional(CONF_ENTITY_ID): cv.string, 48 | vol.Optional(CONF_POWER_DECIMALS): cv.positive_int, #deprecated 49 | vol.Optional(CONF_SETTINGS, default={}): SETTINGS_SCHEMA 50 | }) 51 | 52 | STEP_SCHEMA = vol.Schema({ 53 | vol.Optional(CONF_OBJECT_ID_PREFIX, 54 | default=DEFAULT_OBJECT_ID_PREFIX): str, 55 | }) 56 | 57 | CONFIG_SCHEMA_ROOT = vol.Schema({ 58 | vol.Optional(CONF_IGMPFIX, 59 | default=DEFAULT_IGMPFIX): cv.boolean, 60 | vol.Optional(CONF_SHOW_ID_IN_NAME, 61 | default=DEFAULT_SHOW_ID_IN_NAME): cv.boolean, 62 | vol.Optional(CONF_DISCOVERY, 63 | default=DEFAULT_DISCOVERY): cv.boolean, 64 | vol.Optional(CONF_OBJECT_ID_PREFIX, 65 | default=DEFAULT_OBJECT_ID_PREFIX): cv.string, 66 | vol.Optional(CONF_USERNAME): cv.string, 67 | vol.Optional(CONF_PASSWORD): cv.string, 68 | vol.Optional(CONF_DEVICES, 69 | default=[]): vol.All(cv.ensure_list, [DEVICE_SCHEMA]), 70 | vol.Optional(CONF_VERSION, 71 | default=False): cv.boolean, 72 | vol.Optional(CONF_WIFI_SENSOR): cv.boolean, #deprecated 73 | vol.Optional(CONF_UPTIME_SENSOR): cv.boolean, #deprecated 74 | vol.Optional(CONF_UPGRADE_SWITCH, default=True): cv.boolean, 75 | vol.Optional(CONF_UPGRADE_BETA_SWITCH, default=False): cv.boolean, 76 | vol.Optional(CONF_UNAVALABLE_AFTER_SEC, default=90) : cv.positive_int, 77 | vol.Optional(CONF_SENSORS, default=DEFAULT_SENSORS): 78 | vol.All(cv.ensure_list, [vol.In(ALL_SENSORS_W_EXTRA)]), 79 | vol.Optional(CONF_ATTRIBUTES, default=list(DEFAULT_ATTRIBUTES)): 80 | vol.All(cv.ensure_list, 81 | [vol.In(ALL_ATTRIBUTES | EXTRA_ATTRIBUTES)]), 82 | vol.Optional(CONF_ADDITIONAL_INFO, 83 | default=True): cv.boolean, 84 | vol.Optional(CONF_SCAN_INTERVAL, 85 | default=DEFAULT_SCAN_INTERVAL): cv.positive_int, 86 | vol.Optional(CONF_POWER_DECIMALS): cv.positive_int, #deprecated 87 | vol.Optional(CONF_LOCAL_PY_SHELLY, default=False): cv.boolean, 88 | vol.Optional(CONF_DEBUG_ENABLE_INFO, default=False): cv.boolean, 89 | vol.Optional(CONF_ONLY_DEVICE_ID) : cv.string, 90 | vol.Optional(CONF_CLOUD_AUTH_KEY) : cv.string, 91 | vol.Optional(CONF_CLOUD_SERVER) : cv.string, 92 | vol.Optional(CONF_TMPL_NAME) : cv.string, 93 | vol.Optional(CONF_DISCOVER_BY_IP, default=[]): 94 | vol.All(cv.ensure_list, [cv.string]), 95 | vol.Optional(CONF_MDNS, default=DEFAULT_MDNS): cv.boolean, 96 | vol.Optional(CONF_HOST_IP, default='') : cv.string, 97 | vol.Optional(CONF_SETTINGS, default={}): SETTINGS_SCHEMA, 98 | vol.Optional(CONF_MQTT_PORT): cv.positive_int, 99 | vol.Optional(CONF_MQTT_SERVER_HOST, default=0): cv.string, 100 | vol.Optional(CONF_MQTT_SERVER_PORT, default=1883): cv.positive_int, 101 | vol.Optional(CONF_MQTT_SERVER_USERNAME, default=''): cv.string, 102 | vol.Optional(CONF_MQTT_SERVER_PASSWORD, default=''): cv.string 103 | }) 104 | 105 | CONFIG_SCHEMA = vol.Schema({ 106 | DOMAIN: CONFIG_SCHEMA_ROOT 107 | }, extra=vol.ALLOW_EXTRA) 108 | -------------------------------------------------------------------------------- /custom_components/shelly/cover.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shelly platform for the cover component. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/shelly/ 6 | """ 7 | 8 | #pylint: disable=import-error 9 | from homeassistant.components.cover import (ATTR_POSITION, 10 | SUPPORT_CLOSE, 11 | SUPPORT_OPEN, SUPPORT_STOP, 12 | SUPPORT_SET_POSITION) 13 | 14 | try: 15 | from homeassistant.components.cover import CoverEntity 16 | except: 17 | from homeassistant.components.cover import \ 18 | CoverDevice as CoverEntity 19 | 20 | from .device import ShellyDevice 21 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 22 | 23 | #def setup_platform(hass, _config, add_devices, discovery_info=None): 24 | # """Set up the Shelly cover platform.""" 25 | # dev = get_device_from_hass(hass, discovery_info) 26 | # add_devices([ShellyCover(dev, instance)]) 27 | 28 | async def async_setup_entry(hass, _config_entry, async_add_entities): 29 | """Set up Shelly cover dynamically.""" 30 | async def async_discover_cover(dev, instance): 31 | """Discover and add a discovered cover.""" 32 | async_add_entities([ShellyCover(dev, instance)]) 33 | 34 | async_dispatcher_connect( 35 | hass, 36 | "shelly_new_cover", 37 | async_discover_cover 38 | ) 39 | 40 | class ShellyCover(ShellyDevice, CoverEntity): 41 | """Shelly cover device.""" 42 | 43 | def __init__(self, dev, instance): 44 | """Initialize the cover.""" 45 | ShellyDevice.__init__(self, dev, instance) 46 | self._position = None 47 | self._last_direction = None 48 | self._motion_state = None 49 | self._support_position = None 50 | self._state = None 51 | self._master_unit = True 52 | self.update() 53 | 54 | @property 55 | def should_poll(self): 56 | """No polling needed.""" 57 | return True 58 | 59 | @property 60 | def current_cover_position(self): 61 | """Return current position""" 62 | if self._support_position: 63 | return self._position 64 | 65 | return None 66 | 67 | @property 68 | def is_closed(self): 69 | """Return if the cover is closed or not.""" 70 | if self._support_position: 71 | return self._position == 0 72 | 73 | return None 74 | 75 | @property 76 | def is_closing(self): 77 | """Return if the cover is closing.""" 78 | return self._motion_state == "close" 79 | 80 | @property 81 | def is_opening(self): 82 | """Return if the cover is opening.""" 83 | return self._motion_state == "open" 84 | 85 | @property 86 | def supported_features(self): 87 | """Flag supported features.""" 88 | supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP 89 | if self._support_position: 90 | supported_features |= SUPPORT_SET_POSITION 91 | return supported_features 92 | 93 | def close_cover(self, **_kwargs): 94 | """Close the cover.""" 95 | self._dev.down() 96 | 97 | def open_cover(self, **_kwargs): 98 | """Open the cover.""" 99 | self._dev.up() 100 | 101 | def set_cover_position(self, **kwargs): 102 | """Move the cover to a specific position.""" 103 | pos = kwargs[ATTR_POSITION] 104 | self._dev.set_position(pos) 105 | self._position = pos 106 | self._update_ha_state() 107 | 108 | def stop_cover(self, **_kwargs): 109 | """Stop the cover.""" 110 | self._dev.stop() 111 | 112 | def update(self): 113 | """Fetch new state data for this light. 114 | This is the only method that should fetch new data for Home Assistant. 115 | """ 116 | self._state = self._dev.state 117 | self._position = self._dev.position 118 | self._last_direction = self._dev.last_direction 119 | self._motion_state = self._dev.motion_state 120 | self._support_position = self._dev.support_position 121 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /customize.yaml: -------------------------------------------------------------------------------- 1 | cover.secondary_garage_door: 2 | icon: mdi:garage 3 | cover.primary_garage_door: 4 | icon: mdi:garage 5 | cover.primary_garage_door_2: 6 | icon: mdi:garage 7 | -------------------------------------------------------------------------------- /groups.yaml: -------------------------------------------------------------------------------- 1 | kitchen_lights: 2 | name: Kitchen Lights 3 | entities: 4 | - light.kitchen_bar_lt_level 5 | - light.kitchen_glass_lt_level 6 | - light.kitchen_sink_lt_level 7 | -------------------------------------------------------------------------------- /harmony_10977089.conf: -------------------------------------------------------------------------------- 1 | { 2 | "Activities": { 3 | "-1": "PowerOff", 4 | "44666015": "Play Xbox" 5 | }, 6 | "Devices": { 7 | "Surround Sound": { 8 | "commands": [ 9 | "PowerOff", 10 | "PowerOn", 11 | "PowerToggle", 12 | "Mute", 13 | "VolumeDown", 14 | "VolumeUp", 15 | "DirectionDown", 16 | "DirectionLeft", 17 | "DirectionRight", 18 | "DirectionUp", 19 | "Select", 20 | "Stop", 21 | "Play", 22 | "Pause", 23 | "SkipBack", 24 | "SkipForward", 25 | "Back", 26 | "FM", 27 | "PresetPrev", 28 | "PresetNext", 29 | "Sleep", 30 | "Display", 31 | "AmpMenu", 32 | "Home", 33 | "InputBd/Dvd", 34 | "InputBluetooth", 35 | "InputGame", 36 | "InputSa-Cd/Cd", 37 | "InputSat/Catv", 38 | "InputTv", 39 | "InputUsb", 40 | "InputVideo", 41 | "Memory", 42 | "PureDirect", 43 | "SF-Movie", 44 | "SF-Music" 45 | ], 46 | "id": "69831862" 47 | }, 48 | "Vizio TV": { 49 | "commands": [ 50 | "PowerOff", 51 | "PowerOn", 52 | "PowerToggle", 53 | "Enter", 54 | ".", 55 | "-", 56 | "0", 57 | "1", 58 | "2", 59 | "3", 60 | "4", 61 | "5", 62 | "6", 63 | "7", 64 | "8", 65 | "9", 66 | "Mute", 67 | "VolumeDown", 68 | "VolumeUp", 69 | "ChannelPrev", 70 | "ChannelDown", 71 | "ChannelUp", 72 | "DirectionDown", 73 | "DirectionLeft", 74 | "DirectionRight", 75 | "DirectionUp", 76 | "OK", 77 | "Stop", 78 | "Play", 79 | "Rewind", 80 | "Pause", 81 | "FastForward", 82 | "Record", 83 | "Menu", 84 | "Back", 85 | "Pic", 86 | "Sleep", 87 | "Guide", 88 | "Info", 89 | "Exit", 90 | "Wide", 91 | "Pic", 92 | "Netflix", 93 | "Amazon", 94 | "ClosedCaption", 95 | "iHeartRadio", 96 | "InputAv", 97 | "InputComponent", 98 | "InputHdmi1", 99 | "InputHdmi2", 100 | "InputHdmi3", 101 | "InputHdmi4", 102 | "InputHdmi5", 103 | "InputNext", 104 | "InputTv", 105 | "Media", 106 | "VIA", 107 | "Vudu" 108 | ], 109 | "id": "46621965" 110 | }, 111 | "XONE-BINARYNEXU": { 112 | "commands": [ 113 | "PowerOff", 114 | "PowerOn", 115 | "PowerToggle", 116 | ".", 117 | "0", 118 | "1", 119 | "2", 120 | "3", 121 | "4", 122 | "5", 123 | "6", 124 | "7", 125 | "8", 126 | "9", 127 | "Clear", 128 | "Mute", 129 | "VolumeDown", 130 | "VolumeUp", 131 | "ChannelDown", 132 | "ChannelUp", 133 | "DirectionDown", 134 | "DirectionLeft", 135 | "DirectionRight", 136 | "DirectionUp", 137 | "OK", 138 | "Stop", 139 | "Play", 140 | "Rewind", 141 | "Eject", 142 | "Pause", 143 | "FastForward", 144 | "Record", 145 | "SkipBack", 146 | "SkipForward", 147 | "Menu", 148 | "Subtitle", 149 | "Back", 150 | "LiveTV", 151 | "A", 152 | "B", 153 | "X", 154 | "Y", 155 | "Green", 156 | "Red", 157 | "Blue", 158 | "Yellow", 159 | "Info", 160 | "AppChannels", 161 | "DVR", 162 | "LastChannel", 163 | "Movies", 164 | "OneGuide", 165 | "Play/Pause", 166 | "Replay", 167 | "Standby", 168 | "TVListings", 169 | "TVShows", 170 | "View", 171 | "Xbox" 172 | ], 173 | "id": "46622038" 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /image/60b08011c64f76b31565000dccd7186b/512x512: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/image/60b08011c64f76b31565000dccd7186b/512x512 -------------------------------------------------------------------------------- /image/60b08011c64f76b31565000dccd7186b/original: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/image/60b08011c64f76b31565000dccd7186b/original -------------------------------------------------------------------------------- /image/984422fe027bc3759750871a0b631c59/512x512: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/image/984422fe027bc3759750871a0b631c59/512x512 -------------------------------------------------------------------------------- /image/984422fe027bc3759750871a0b631c59/original: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/image/984422fe027bc3759750871a0b631c59/original -------------------------------------------------------------------------------- /image/edc1f990c6e31085b7bba1bee8b888f3/512x512: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/image/edc1f990c6e31085b7bba1bee8b888f3/512x512 -------------------------------------------------------------------------------- /image/edc1f990c6e31085b7bba1bee8b888f3/original: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/image/edc1f990c6e31085b7bba1bee8b888f3/original -------------------------------------------------------------------------------- /lights/HolidayLights.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Holiday Lights" 3 | command_topic: "RoofLightsMCU/power" 4 | state_topic: "RoofLightsMCU/powerState" 5 | brightness_command_topic: "RoofLightsMCU/brightness" 6 | brightness_state_topic: "RoofLightsMCU/brightnessState" 7 | brightness_scale: 255 8 | white_value_command_topic: "RoofLightsMCU/modifier" 9 | white_value_state_topic: "RoofLightsMCU/modifierState" 10 | white_value_scale: 500 11 | effect_command_topic: "RoofLightsMCU/effect" 12 | effect_state_topic: "RoofLightsMCU/effectState" 13 | effect_list: 14 | - Color_Chase 15 | - Color_Glitter 16 | - Single_Race 17 | - Double_Crash 18 | - Rainbow 19 | - Blocked_Colors 20 | - BPM 21 | - Twinkle 22 | - Fire 23 | - Fill_Solid 24 | - Spooky_Eyes 25 | - LED_Locator 26 | retain: true -------------------------------------------------------------------------------- /lights/HolidayLightsColor1.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Holiday Color 1" 3 | command_topic: "RoofLightsMCU/ColorPower" 4 | state_topic: "RoofLightsMCU/powerState" 5 | rgb_command_topic: "RoofLightsMCU/color1" 6 | rgb_state_topic: "RoofLightsMCU/color1State" 7 | retain: true -------------------------------------------------------------------------------- /lights/HolidayLightsColor2.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Holiday Color 2" 3 | command_topic: "RoofLightsMCU/ColorPower" 4 | state_topic: "RoofLightsMCU/powerState" 5 | rgb_command_topic: "RoofLightsMCU/color2" 6 | rgb_state_topic: "RoofLightsMCU/color2State" 7 | retain: true -------------------------------------------------------------------------------- /lights/HolidayLightsColor3.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Holiday Color 3" 3 | command_topic: "RoofLightsMCU/ColorPower" 4 | state_topic: "RoofLightsMCU/powerState" 5 | rgb_command_topic: "RoofLightsMCU/color3" 6 | rgb_state_topic: "RoofLightsMCU/color3State" 7 | retain: true -------------------------------------------------------------------------------- /lights/HolidayLightsGlitter.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Holiday Glitter" 3 | state_topic: "RoofLightsMCU/glitter/state" 4 | command_topic: "RoofLightsMCU/addEffects" 5 | payload_on: "Glitter On" 6 | payload_off: "Glitter Off" 7 | rgb_command_topic: "RoofLightsMCU/glitterColor" 8 | rgb_state_topic: "RoofLightsMCU/glitterColorState" 9 | white_value_command_topic: "RoofLightsMCU/glitterChance" 10 | white_value_state_topic: "RoofLightsMCU/glitterChanceState" 11 | white_value_scale: 255 12 | retain: true -------------------------------------------------------------------------------- /lights/HolidayLightsLightning.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Holiday Lightning" 3 | state_topic: "RoofLightsMCU/lightning/state" 4 | command_topic: "RoofLightsMCU/addEffects" 5 | payload_on: "Lightning On" 6 | payload_off: "Lightning Off" 7 | white_value_command_topic: "RoofLightsMCU/lightningChance" 8 | white_value_state_topic: "RoofLightsMCU/lightningChanceState" 9 | white_value_scale: 500 10 | retain: true -------------------------------------------------------------------------------- /lights/OfficeLEDBulbs.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Office Light Bulbs" 3 | command_topic: "cmnd/OfficeOverheadLEDBulbs/POWER" 4 | state_topic: "tele/OfficeOverheadLEDBulbs/STATE" 5 | state_value_template: "{{value_json.POWER}}" 6 | availability_topic: "tele/OfficeOverheadLEDBulbs/LWT" 7 | brightness_command_topic: "cmnd/OfficeOverheadLEDBulbs/Dimmer" 8 | brightness_state_topic: "tele/OfficeOverheadLEDBulbs/STATE" 9 | brightness_scale: 100 10 | on_command_type: "last" 11 | brightness_value_template: "{{value_json.Dimmer}}" 12 | color_temp_command_topic: "cmnd/OfficeOverheadLEDBulbs/CT" 13 | color_temp_state_topic: "tele/OfficeOverheadLEDBulbs/STATE" 14 | color_temp_value_template: "{{value_json.CT}}" 15 | rgb_command_topic: "cmnd/OfficeOverheadLEDBulbs/Color" 16 | rgb_state_topic: "tele/OfficeOverheadLEDBulbs/STATE" 17 | rgb_value_template: "{{value_json.Color.split(',')[0:3]|join(',')}}" 18 | effect_command_topic: "cmnd/OfficeOverheadLEDBulbs/Scheme" 19 | effect_state_topic: "tele/OfficeOverheadLEDBulbs/STATE" 20 | effect_value_template: "{{value_json.Scheme}}" 21 | effect_list: 22 | - 0 23 | - 1 24 | - 2 25 | - 3 26 | - 4 27 | payload_on: "ON" 28 | payload_off: "OFF" 29 | payload_available: "Online" 30 | payload_not_available: "Offline" 31 | qos: 1 32 | retain: false -------------------------------------------------------------------------------- /lights/Shelly3DPrinterRoomLightRelay.yaml: -------------------------------------------------------------------------------- 1 | # Downstair Bathroom Light 2 | platform: mqtt 3 | name: "3D Printing Room Light" 4 | state_topic: "shellies/Shelly3DPrinterRoomLightFan/relay/0" 5 | command_topic: "shellies/Shelly3DPrinterRoomLightFan/relay/0/command" 6 | payload_on: "on" 7 | payload_off: "off" 8 | retain: false -------------------------------------------------------------------------------- /lights/ShellyDownstairsBathroomLightRelay.yaml: -------------------------------------------------------------------------------- 1 | # Downstair Bathroom Light 2 | platform: mqtt 3 | name: "Downstairs Bathroom Light" 4 | state_topic: "shellies/shellyswitch25-68F3DB/relay/0" 5 | command_topic: "shellies/shellyswitch25-68F3DB/relay/0/command" 6 | payload_on: "on" 7 | payload_off: "off" 8 | retain: false -------------------------------------------------------------------------------- /lights/ShellyMasterBathroomMainLightRelay.yaml: -------------------------------------------------------------------------------- 1 | # Downstair Bathroom Light 2 | platform: mqtt 3 | name: "Master Bathroom Light" 4 | state_topic: "shellies/ShellyMasterBathroomLight/relay/0" 5 | command_topic: "shellies/ShellyMasterBathroomLight/relay/0/command" 6 | payload_on: "on" 7 | payload_off: "off" 8 | retain: false -------------------------------------------------------------------------------- /lights/ShellyMasterBathroomVentLightRelay.yaml: -------------------------------------------------------------------------------- 1 | # Downstair Bathroom Light 2 | platform: mqtt 3 | name: "Master Bathroom Vent Light" 4 | state_topic: "shellies/ShellyMasterBathroomVent/relay/1" 5 | command_topic: "shellies/ShellyMasterBathroomVent/relay/1/command" 6 | payload_on: "on" 7 | payload_off: "off" 8 | retain: false -------------------------------------------------------------------------------- /scenes.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/scenes.yaml -------------------------------------------------------------------------------- /scripts.yaml: -------------------------------------------------------------------------------- 1 | sunrise_script: 2 | alias: Sunrise Script 3 | sequence: 4 | - service: mqtt.publish 5 | data: 6 | topic: NodeRed/routine/sunrise 7 | payload: 'ON' 8 | qos: '1' 9 | mode: single 10 | icon: mdi:weather-sunset-up 11 | sunset_procedure: 12 | alias: Sunset Script 13 | sequence: 14 | - service: mqtt.publish 15 | data: 16 | topic: NodeRed/routine/sunset1hourbefore 17 | payload: 'ON' 18 | qos: '1' 19 | mode: single 20 | icon: mdi:weather-sunset-down 21 | bedtime_joshua: 22 | alias: Bedtime Joshua 23 | sequence: 24 | - service: mqtt.publish 25 | data: 26 | topic: NodeRed/routine/bedtime 27 | qos: '1' 28 | mode: single 29 | icon: mdi:bed 30 | '1590965567595': 31 | alias: Surround Sound - Mute 32 | sequence: 33 | - data: 34 | command: 35 | - Mute 36 | device: 69831862 37 | entity_id: remote.living_room 38 | entity_id: remote.living_room 39 | service: remote.send_command 40 | -------------------------------------------------------------------------------- /sensors/HolidaySensor.yaml: -------------------------------------------------------------------------------- 1 | platform: template 2 | sensors: 3 | season: 4 | friendly_name: "Season" 5 | value_template: > 6 | {% if now().strftime("%B") == "February" and now().strftime("%-d")|int == 14 %} 7 | VALENTINES 8 | {% elif now().strftime("%B") == "July" and now().strftime("%-d")|int > 4 %} 9 | INDEPENDENCE 10 | {% elif now().strftime("%B") == "October" %} 11 | HALLOWEEN 12 | {% elif now().strftime("%B") == "November" and now().strftime("%-d")|int > 27 %} 13 | CHRISTMAS 14 | {% elif now().strftime("%B") == "December" %} 15 | CHRISTMAS 16 | {% else %} 17 | OFF 18 | {% endif %} -------------------------------------------------------------------------------- /sensors/NetData.yaml: -------------------------------------------------------------------------------- 1 | platform: netdata 2 | host: !secret docker_system 3 | port: '19999' 4 | name: NetData 5 | resources: 6 | "NetData CPU": 7 | data_group: 'cgroup_netdata.cpu_limit' 8 | element: 'used' 9 | icon: mdi:chip 10 | "Plex CPU": 11 | data_group: 'cgroup_plex-server.cpu_limit' 12 | element: 'used' 13 | icon: mdi:chip 14 | "Sonarr CPU": 15 | data_group: 'cgroup_sonarr.cpu_limit' 16 | element: 'used' 17 | icon: mdi:chip 18 | "Radarr CPU": 19 | data_group: 'cgroup_radarr.cpu_limit' 20 | element: 'used' 21 | icon: mdi:chip 22 | "Bazarr CPU": 23 | data_group: 'cgroup_bazarr.cpu_limit' 24 | element: 'used' 25 | icon: mdi:chip 26 | "Lidarr CPU": 27 | data_group: 'cgroup_lidarr.cpu_limit' 28 | element: 'used' 29 | icon: mdi:chip 30 | "NextCloud CPU": 31 | data_group: 'cgroup_nextcloud.cpu_limit' 32 | element: 'used' 33 | icon: mdi:chip 34 | "Home Assistant CPU": 35 | data_group: 'cgroup_nextcloud.cpu_limit' 36 | element: 'used' 37 | icon: mdi:chip 38 | "Portainer CPU": 39 | data_group: 'cgroup_portainer.cpu_limit' 40 | element: 'used' 41 | icon: mdi:chip 42 | "Duplicati CPU": 43 | data_group: 'cgroup_duplicati.cpu_limit' 44 | element: 'used' 45 | icon: mdi:chip 46 | "MQTT CPU": 47 | data_group: 'cgroup_mqtt.cpu_limit' 48 | element: 'used' 49 | icon: mdi:chip 50 | "Ombi CPU": 51 | data_group: 'cgroup_ombi.cpu_limit' 52 | element: 'used' 53 | icon: mdi:chip 54 | "NodeRed CPU": 55 | data_group: 'cgroup_node-red.cpu_limit' 56 | element: 'used' 57 | icon: mdi:chip 58 | "Nzbget CPU": 59 | data_group: 'cgroup_nzbget.cpu_limit' 60 | element: 'used' 61 | icon: mdi:chip 62 | "MariaDB CPU": 63 | data_group: 'cgroup_mariadb.cpu_limit' 64 | element: 'used' 65 | icon: mdi:chip 66 | "Jackett CPU": 67 | data_group: 'cgroup_jackett.cpu_limit' 68 | element: 'used' 69 | icon: mdi:chip 70 | "Hydra2 CPU": 71 | data_group: 'cgroup_hydra2.cpu_limit' 72 | element: 'used' 73 | icon: mdi:chip 74 | "Bitwarden CPU": 75 | data_group: 'cgroup_bitwarden.cpu_limit' 76 | element: 'used' 77 | icon: mdi:chip 78 | "Transmission CPU": 79 | data_group: 'cgroup_transmission.cpu_limit' 80 | element: 'used' 81 | icon: mdi:chip 82 | "Guacamole CPU": 83 | data_group: 'cgroup_guacamole.cpu_limit' 84 | element: 'used' 85 | icon: mdi:chip 86 | "Tdarr CPU": 87 | data_group: 'cgroup_tdarr.cpu_limit' 88 | element: 'used' 89 | icon: mdi:chip 90 | "Unifi Controller CPU": 91 | data_group: 'cgroup_unifi-controller.cpu_limit' 92 | element: 'used' 93 | icon: mdi:chip 94 | "Watchtower CPU": 95 | data_group: 'cgroup_watchtower.cpu_limit' 96 | element: 'used' 97 | icon: mdi:chip 98 | "Traefik CPU": 99 | data_group: 'cgroup_traefik.cpu_limit' 100 | element: 'used' 101 | icon: mdi:chip 102 | "TasmoBackup CPU": 103 | data_group: 'cgroup_tasmobackup.cpu_limit' 104 | element: 'used' 105 | icon: mdi:chip 106 | "Organizr CPU": 107 | data_group: 'cgroup_organizr.cpu_limit' 108 | element: 'used' 109 | icon: mdi:chip 110 | "PHPMyAdmin CPU": 111 | data_group: 'cgroup_phpmyadmin.cpu_limit' 112 | element: 'used' 113 | icon: mdi:chip 114 | "VSCode CPU": 115 | data_group: 'cgroup_code-server.cpu_limit' 116 | element: 'used' 117 | icon: mdi:chip -------------------------------------------------------------------------------- /sensors/ShellyHTMasterBathroomBattery.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: Master Bathroom Battery 3 | state_topic: "shellies/ShellyHTMasterBathroom/sensor/battery" -------------------------------------------------------------------------------- /sensors/ShellyHTMasterBathroomHumidity.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: Master Bathroom Humidity 3 | state_topic: "shellies/ShellyHTMasterBathroom/sensor/humidity" -------------------------------------------------------------------------------- /sensors/ShellyHTMasterBathroomTemp.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: Master Bathroom Temperature 3 | state_topic: "shellies/ShellyHTMasterBathroom/sensor/temperature" -------------------------------------------------------------------------------- /sensors/TasmotaLatestVersionSensor.yaml: -------------------------------------------------------------------------------- 1 | platform: rest 2 | name: Tasmota Latest Version 3 | resource: https://api.github.com/repos/arendst/Sonoff-Tasmota/releases/latest 4 | value_template: '{{ value_json.tag_name }}' 5 | headers: 6 | Accept: application/vnd.github.v3+json 7 | Content-Type: application/json 8 | User-Agent: Home Assistant REST sensor -------------------------------------------------------------------------------- /sensors/WorkComputerWFH.yaml: -------------------------------------------------------------------------------- 1 | platform: template 2 | sensors: 3 | joshua_work_from_home: 4 | friendly_name: "Working From Home" 5 | value_template: > 6 | {% if is_state('device_tracker.dtna_surface_pro_6_ctnal0066412565', 'home') %} 7 | true 8 | {% else %} 9 | false 10 | {% endif %} -------------------------------------------------------------------------------- /sensors/systemmonitor.yaml: -------------------------------------------------------------------------------- 1 | platform: systemmonitor 2 | resources: 3 | - type: processor_use 4 | - type: last_boot 5 | - type: memory_free 6 | - type: memory_use_percent 7 | - type: memory_use 8 | - type: network_in 9 | arg: eth0 10 | - type: network_out 11 | arg: eth0 12 | - type: throughput_network_in 13 | arg: eth0 14 | - type: throughput_network_out 15 | arg: eth0 16 | - type: packets_in 17 | arg: eth0 18 | - type: packets_out 19 | arg: eth0 20 | - type: ipv4_address 21 | arg: eth0 22 | - type: ipv6_address 23 | arg: eth0 24 | - type: disk_use_percent 25 | arg: / 26 | - type: disk_use 27 | arg: / 28 | - type: disk_free 29 | arg: / -------------------------------------------------------------------------------- /switches/AddGlitter.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Add Glitter" 3 | state_topic: "houseleds/glitter/state" 4 | command_topic: "houseleds/addEffects" 5 | payload_on: "Glitter On" 6 | payload_off: "Glitter Off" 7 | retain: false -------------------------------------------------------------------------------- /switches/AddLightning.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Add Lightning" 3 | state_topic: "houseleds/lightning/state" 4 | command_topic: "houseleds/addEffects" 5 | payload_on: "Lightning On" 6 | payload_off: "Lightning Off" 7 | retain: false -------------------------------------------------------------------------------- /switches/AudioEffects.yaml: -------------------------------------------------------------------------------- 1 | platform: mqtt 2 | name: "Audio Effects" 3 | state_topic: "houseleds/audio/state" 4 | command_topic: "houseleds/addEffects" 5 | payload_on: "Audio On" 6 | payload_off: "Audio Off" 7 | retain: false -------------------------------------------------------------------------------- /switches/Shelly3DPrinterRoomFanRelay.yaml: -------------------------------------------------------------------------------- 1 | # Downstair Bathroom Light 2 | platform: mqtt 3 | name: "3D Printing Room Fan" 4 | state_topic: "shellies/Shelly3DPrinterRoomLightFan/relay/1" 5 | command_topic: "shellies/Shelly3DPrinterRoomLightFan/relay/1/command" 6 | payload_on: "on" 7 | payload_off: "off" 8 | retain: false -------------------------------------------------------------------------------- /switches/ShellyDownstairsBathroomVentRelay.yaml: -------------------------------------------------------------------------------- 1 | # Downstair Bathroom Vent 2 | platform: mqtt 3 | name: "Downstairs Bathroom Vent" 4 | state_topic: "shellies/shellyswitch25-68F3DB/relay/1" 5 | command_topic: "shellies/shellyswitch25-68F3DB/relay/1/command" 6 | payload_on: "on" 7 | payload_off: "off" 8 | retain: false -------------------------------------------------------------------------------- /switches/ShellyMasterBathroomVentRelay.yaml: -------------------------------------------------------------------------------- 1 | # Downstair Bathroom Vent 2 | platform: mqtt 3 | name: "Master Bathroom Vent" 4 | state_topic: "shellies/ShellyMasterBathroomVent/relay/0" 5 | command_topic: "shellies/ShellyMasterBathroomVent/relay/0/command" 6 | payload_on: "on" 7 | payload_off: "off" 8 | retain: false -------------------------------------------------------------------------------- /themes/midnight/midnight.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # Midnight Theme 4 | # 5 | midnight: 6 | accent-color: "#E45E65" 7 | card-background-color: "var(--primary-background-color)" 8 | dark-primary-color: "var(--accent-color)" 9 | disabled-text-color: "#7F848E" 10 | divider-color: "rgba(0, 0, 0, .12)" 11 | google-blue-500: "#4285f4" 12 | google-green-500: "#39E949" 13 | google-red-500: "#E45E65" 14 | google-yellow-500: "#f4b400" 15 | ha-card-background: "#434954" 16 | label-badge-background-color: "#2E333A" 17 | label-badge-blue: "#039be5" 18 | label-badge-border-color: "green" 19 | label-badge-green: "#0DA035" 20 | label-badge-grey: "var(--paper-grey-500)" 21 | label-badge-red: "var(--accent-color)" 22 | label-badge-text-color: "var(--primary-text-color)" 23 | label-badge-yellow: "#f4b400" 24 | light-primary-color: "var(--accent-color)" 25 | markdown-code-background-color: "var(--paper-listbox-background-color)" 26 | paper-card-header-color: "var(--accent-color)" 27 | paper-dialog-background-color: "#434954" 28 | paper-grey-200: "#414A59" 29 | paper-grey-50: "var(--primary-text-color)" 30 | paper-grey-500: "#9e9e9e" 31 | paper-item-icon_-_color: "green" 32 | paper-item-icon-active-color: "#F9C536" 33 | paper-item-icon-color: "var(--primary-text-color)" 34 | paper-item-selected_-_background-color: "#434954" 35 | paper-listbox-background-color: "#2E333A" 36 | paper-listbox-color: "var(--primary-color)" 37 | paper-slider-active-color: "var(--accent-color)" 38 | paper-slider-container-color: "linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat" 39 | paper-slider-disabled-active-color: "var(--disabled-text-color)" 40 | paper-slider-disabled-secondary-color: "var(--disabled-text-color)" 41 | paper-slider-knob-color: "var(--accent-color)" 42 | paper-slider-knob-start-color: "var(--accent-color)" 43 | paper-slider-pin-color: "var(--accent-color)" 44 | paper-slider-secondary-color: "var(--secondary-background-color)" 45 | paper-tabs-selection-bar-color: "green" 46 | paper-toggle-button-checked-bar-color: "var(--accent-color)" 47 | paper-toggle-button-checked-button-color: "var(--accent-color)" 48 | paper-toggle-button-checked-ink-color: "var(--accent-color)" 49 | paper-toggle-button-unchecked-bar-color: "var(--disabled-text-color)" 50 | paper-toggle-button-unchecked-button-color: "var(--disabled-text-color)" 51 | paper-toggle-button-unchecked-ink-color: "var(--disabled-text-color)" 52 | primary-background-color: "#383C45" 53 | primary-color: "#5294E2" 54 | primary-text-color: "#FFFFFF" 55 | secondary-background-color: "#383C45" 56 | secondary-text-color: "#5294E2" 57 | sidebar-background-color: "var(--paper-listbox-background-color)" 58 | sidebar-icon-color: "rgba(255, 255, 255, 0.70)" 59 | sidebar-selected-icon-color: "var(--primary-color)" 60 | sidebar-selected-text-color: "var(--primary-text-color)" 61 | sidebar-text-color: "var(--primary-text-color)" 62 | slider-bar-color: "var(--disabled-text-color)" 63 | slider-color: "var(--primary-color)" 64 | slider-secondary-color: "var(--light-primary-color)" 65 | st-mode-active-background: "var(--dark-primary-color)" 66 | st-mode-background: "var(--primary-background-color)" 67 | state-icon-active-color: "#FDD835" 68 | state-icon-color: "#44739e" 69 | state-icon-unavailable-color: "var(--disabled-text-color)" 70 | switch-checked-color: "var(--paper-toggle-button-checked-button-color)" 71 | switch-unchecked-button-color: "var(--disabled-text-color)" 72 | switch-unchecked-color: "var(--disabled-text-color)" 73 | switch-unchecked-track-color: "var(--disabled-text-color)" 74 | table-row-alternative-background-color: "#3E424B" 75 | table-row-background-color: "#353840" 76 | text-primary-color: "var(--primary-text-color)" 77 | toggle-button-color: "var(--primary-color)" 78 | -------------------------------------------------------------------------------- /tts/29dc8ba06b5c447f087b9bcd92b400a1e1ccf2ad_ru-ru_a9c18110b0_cloud.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/tts/29dc8ba06b5c447f087b9bcd92b400a1e1ccf2ad_ru-ru_a9c18110b0_cloud.mp3 -------------------------------------------------------------------------------- /tts/bfc702d80671b6944df07c65c91b383cec555c8a_en-us_a9c18110b0_cloud.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/tts/bfc702d80671b6944df07c65c91b383cec555c8a_en-us_a9c18110b0_cloud.mp3 -------------------------------------------------------------------------------- /tts/f26a333f608779251c79ad6524c1f8758bb67f01_en-us_a9c18110b0_cloud.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/tts/f26a333f608779251c79ad6524c1f8758bb67f01_en-us_a9c18110b0_cloud.mp3 -------------------------------------------------------------------------------- /www/community/lovelace-auto-entities/auto-entities.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/lovelace-auto-entities/auto-entities.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-auto-entities/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import json from "@rollup/plugin-json"; 3 | import typescript from "rollup-plugin-typescript2"; 4 | import { terser } from "rollup-plugin-terser"; 5 | import babel from "@rollup/plugin-babel"; 6 | 7 | const dev = process.env.ROLLUP_WATCH; 8 | 9 | export default { 10 | input: "src/main.ts", 11 | output: { 12 | file: "auto-entities.js", 13 | format: "es", 14 | }, 15 | plugins: [ 16 | nodeResolve(), 17 | json(), 18 | typescript(), 19 | babel({ 20 | exclude: "node_modules/**", 21 | }), 22 | !dev && terser({ format: { comments: false } }), 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /www/community/lovelace-auto-entities/rollup.config.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/lovelace-auto-entities/rollup.config.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-battery-entity-row/battery-entity-row.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/lovelace-battery-entity-row/battery-entity-row.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-multiple-entity-row/multiple-entity-row.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/lovelace-multiple-entity-row/multiple-entity-row.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-template-entity-row/template-entity-row.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/lovelace-template-entity-row/template-entity-row.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-template-entity-row/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/main.js', 5 | mode: 'production', 6 | output: { 7 | filename: 'template-entity-row.js', 8 | path: path.resolve(__dirname) 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /www/community/lovelace-template-entity-row/webpack.config.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/lovelace-template-entity-row/webpack.config.js.gz -------------------------------------------------------------------------------- /www/community/mini-graph-card/mini-graph-card-bundle.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/mini-graph-card/mini-graph-card-bundle.js.gz -------------------------------------------------------------------------------- /www/community/vacuum-card/vacuum-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryn3xus/Home-Assistant-Configuration/52c446b27b2c47f975c8a2f4db8899908a501463/www/community/vacuum-card/vacuum-card.js.gz -------------------------------------------------------------------------------- /zwave_device_config.yaml: -------------------------------------------------------------------------------- 1 | light.mbr_outlet_level: 2 | ignored: false 3 | polling_intensity: 1 4 | light.office_overhead_light_level: 5 | ignored: false 6 | polling_intensity: 1 7 | light.kitchen_sink_lt_level: 8 | ignored: false 9 | polling_intensity: 1 10 | light.back_floodlights_level: 11 | ignored: false 12 | polling_intensity: 1 13 | light.back_porch_light_level: 14 | ignored: false 15 | polling_intensity: 1 16 | switch.bar_outlet_switch: 17 | ignored: false 18 | polling_intensity: 1 19 | sensor.cr10s_s4_energy_3: 20 | ignored: false 21 | polling_intensity: 1 22 | sensor.cr10s_s4_exporting_3: 23 | ignored: false 24 | polling_intensity: 1 25 | sensor.cr10s_s4_interval_5: 26 | ignored: false 27 | polling_intensity: 1 28 | sensor.cr10s_s4_interval_6: 29 | ignored: false 30 | polling_intensity: 1 31 | switch.3dprinter_cr10s_s4: 32 | ignored: false 33 | polling_intensity: 1 34 | sensor.cr10s_s4_power_3: 35 | ignored: false 36 | polling_intensity: 1 37 | sensor.cr10s_s4_power_6: 38 | ignored: false 39 | polling_intensity: 1 40 | sensor.cr10s_s4_previous_reading_5: 41 | ignored: false 42 | polling_intensity: 1 43 | sensor.cr10s_s4_previous_reading_6: 44 | ignored: false 45 | polling_intensity: 1 46 | sensor.ender_3_energy_2: 47 | ignored: false 48 | polling_intensity: 1 49 | sensor.ender_3_exporting_2: 50 | ignored: false 51 | polling_intensity: 1 52 | sensor.ender_3_interval_3: 53 | ignored: false 54 | polling_intensity: 1 55 | sensor.ender_3_interval_4: 56 | ignored: false 57 | polling_intensity: 1 58 | switch.3dprinter_ender3: 59 | ignored: false 60 | polling_intensity: 1 61 | sensor.ender_3_power_2: 62 | ignored: false 63 | polling_intensity: 1 64 | sensor.ender_3_power_5: 65 | ignored: false 66 | polling_intensity: 1 67 | sensor.ender_3_previous_reading_3: 68 | ignored: false 69 | polling_intensity: 1 70 | sensor.ender_3_previous_reading_4: 71 | ignored: false 72 | polling_intensity: 1 73 | switch.ext_lr_outlet_switch: 74 | ignored: false 75 | polling_intensity: 1 76 | light.front_floodlight_level: 77 | ignored: false 78 | polling_intensity: 1 79 | light.front_porch_light_level: 80 | ignored: false 81 | polling_intensity: 1 82 | light.garage_entry_light_level: 83 | ignored: false 84 | polling_intensity: 1 85 | switch.heatingpad: 86 | ignored: false 87 | polling_intensity: 1 88 | sensor.heatpad_energy: 89 | ignored: false 90 | polling_intensity: 1 91 | sensor.heatpad_exporting: 92 | ignored: false 93 | polling_intensity: 1 94 | sensor.heatpad_interval: 95 | ignored: false 96 | polling_intensity: 1 97 | sensor.heatpad_interval_2: 98 | ignored: false 99 | polling_intensity: 1 100 | sensor.heatpad_power: 101 | ignored: false 102 | polling_intensity: 1 103 | sensor.heatpad_power_2: 104 | ignored: false 105 | polling_intensity: 1 106 | sensor.heatpad_previous_reading: 107 | ignored: false 108 | polling_intensity: 1 109 | sensor.heatpad_previous_reading_2: 110 | ignored: false 111 | polling_intensity: 1 112 | light.garage_second_light: 113 | ignored: false 114 | polling_intensity: 1 115 | light.kitchen_bar_lt_level: 116 | ignored: false 117 | polling_intensity: 1 118 | light.kitchen_glass_lt_level: 119 | ignored: false 120 | polling_intensity: 1 121 | switch.living_room_fan_switch: 122 | ignored: false 123 | polling_intensity: 1 124 | light.living_room_ligh_level: 125 | ignored: false 126 | polling_intensity: 1 127 | light.loft_area_light_level: 128 | ignored: false 129 | polling_intensity: 1 130 | switch.mbr_fan_switch: 131 | ignored: false 132 | polling_intensity: 1 133 | light.mbr_light_level: 134 | ignored: false 135 | polling_intensity: 1 136 | light.nook_light_level: 137 | ignored: false 138 | polling_intensity: 1 139 | light.primary_garage_l_level: 140 | ignored: false 141 | polling_intensity: 1 142 | light.stairway_light_level: 143 | ignored: false 144 | polling_intensity: 1 145 | switch.dragon_tech_unknown_type_4447_id_3031_switch: 146 | ignored: false 147 | polling_intensity: 1 148 | switch.front_porch_outl_switch: 149 | ignored: false 150 | polling_intensity: 1 151 | --------------------------------------------------------------------------------