updating configuration
Python application / build (push) Failing after 55s

This commit is contained in:
Your Name
2020-01-30 22:22:18 +03:00
parent eb13a3ab19
commit 194723e228
125 changed files with 191 additions and 6655 deletions
@@ -1,46 +0,0 @@
import appdaemon.plugins.hass.hassapi as hass
class Noolite(hass.Hass):
brightness = 40
force = False
def initialize(self):
self.run_at_sunrise(self.sunrise_cb)
# self.run_at_sunset(self.sunset_cb)
self.listen_state(self.light_change, "group.mainroom_light")
self.listen_event(self.away_mode, "away_mode")
def away_mode(self, event_id, event_args, kwargs):
self.noolite_off()
def light_change(self, entity, attribute, old, new, kwargs):
sun_down = self.sun_down()
if new == "off" and (sun_down or self.force) and self.somebody_home():
self.noolite_on(self.brightness)
else:
self.noolite_off()
def noolite_off(self):
self.log("Turning off salt lamp.")
self.turn_off(self.args['entity_id'])
def noolite_on(self, brightness=120):
self.log("Turning on salt lamp for brightness {}.".format(brightness))
self.turn_on(self.args['entity_id'], brightness=self.brightness)
def sunset_cb(self, kwargs):
if not self.somebody_home():
return
self.noolite_on(self.brightness)
def somebody_home(self):
if 'ha_panel' not in self.args:
return True
ha_panel_state = self.get_state(self.args['ha_panel'])
if ha_panel_state == 'armed_away':
return False
return True
def sunrise_cb(self, kwargs):
self.noolite_off()
@@ -1,5 +0,0 @@
salt_lamp:
module: salt_lamp
class: Noolite
entity_id: light.salt_lamp
ha_panel: alarm_control_panel.ha_alarm
-51
View File
@@ -1,51 +0,0 @@
esphome:
name: bath_light
platform: ESP8266
board: esp01_1m
wifi:
ssid: !secret wifi_24_name
password: !secret wifi_24_pass
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "BathLight Fallback Hotspot"
password: !secret esphome_captive_pass
logger:
api:
password: !secret esphome_pass
ota:
password: !secret esphome_pass
binary_sensor:
- platform: gpio
pin:
number: GPIO14
mode: INPUT_PULLUP
inverted: True
id: button_1
on_press:
then:
- light.toggle: light_1
- platform: status
name: "Bath status"
output:
- platform: gpio
pin: GPIO12
id: relay_1
light:
- platform: binary
name: "bath"
id: light_1
output: relay_1
status_led:
pin:
number: GPIO13
inverted: yes
-40
View File
@@ -1,40 +0,0 @@
# Need to turn off logging because senseair uses uart and have conflicts with logging
logger:
baud_rate: 0
esphome:
name: senseair
platform: ESP8266
board: d1_mini
wifi:
ssid: !secret wifi_24_name
password: !secret wifi_24_pass
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Senseair Fallback Hotspot"
password: !secret esphome_captive_pass
captive_portal:
# Enable Home Assistant API
api:
password: !secret esphome_pass
ota:
password: !secret esphome_pass
# senseair communication uart
uart:
rx_pin: D7
tx_pin: D8
baud_rate: 9600
# hass sensor =)
sensor:
- platform: senseair
co2:
name: "SenseAir CO2 Value"
update_interval: 60s
+137
View File
@@ -0,0 +1,137 @@
#include "esphome.h"
#include <Bounce2.h>
#define CounterNum 2
#define COUNTER_COLD 0
#define COUNTER_HOT 1
static const char *TAG = "watercounter.sensor";
class WaterCountersSensor : public Component, public Sensor, public CustomAPIDevice {
protected:
int CounterPin[CounterNum] = {D1,D2};
float CounterValues[CounterNum] = {};
Bounce CounterBouncer[CounterNum] = {};
public:
Sensor *cold_sensor = new Sensor();
Sensor *hot_sensor = new Sensor();
bool initReadValues = false;
globals::GlobalsComponent<float> *counter_cold_value;
globals::GlobalsComponent<float> *counter_hot_value;
void setup() override {
register_service(&WaterCountersSensor::on_set_counter_values, "set_counter_values",
{"cold_value", "hot_value"});
// init flash variables
counter_cold_value = new globals::GlobalsComponent<float>();
App.register_component(counter_cold_value);
counter_cold_value->set_restore_value(1547432152);
counter_hot_value = new globals::GlobalsComponent<float>();
App.register_component(counter_hot_value);
counter_hot_value->set_restore_value(2087162808);
ESP_LOGD(TAG, "Initializing counters.");
for (int i=0; i<CounterNum; i++) {
pinMode(CounterPin[i], INPUT_PULLUP);
CounterBouncer[i].attach(CounterPin[i]);
CounterBouncer[i].interval(100);
}
ESP_LOGD(TAG, "Initialize done.");
}
void loop() override {
if (is_connected() && !initReadValues) {
ESP_LOGD(TAG, "Reading counters from fs.");
for (int i=0; i<CounterNum; i++) {
readCounterFromFile(i);
sendCounterValue(i);
}
initReadValues = true;
}
for (int i=0; i<CounterNum; i++) {
boolean changed = CounterBouncer[i].update();
if ( changed ) {
int value = CounterBouncer[i].read();
if ( value == LOW) {
ESP_LOGD(TAG, "Got counter %d 'tick'.", i);
ESP_LOGD(TAG, "Incrementing value. Old: %.2f", CounterValues[i]);
CounterValues[i] = CounterValues[i] + 0.01;
ESP_LOGD(TAG, "New: %.2f", CounterValues[i]);
sendCounterValue(i);
writeFile(i);
printState(i);
}
}
}
}
// void update() override {
// for (int i=0; i<CounterNum; i++) {
// sendCounterValue(i);
// }
// }
// --------------- HASS service call handling ----------------------------
void on_set_counter_values(float cold_value, float hot_value)
{
ESP_LOGD(TAG, "Saving values: cold - %.2f, hot - %.2f", cold_value, hot_value);
CounterValues[COUNTER_COLD] = cold_value;
CounterValues[COUNTER_HOT] = hot_value;
for (int i=0; i<CounterNum; i++) {
writeFile(i);
sendCounterValue(i);
}
ESP_LOGD(TAG, "Save success.");
}
// --------------- functions ----------------------------
void readCounterFromFile(int counterId) {
if (counterId == COUNTER_COLD) {
CounterValues[COUNTER_COLD] = counter_cold_value->value();
}else if (counterId == COUNTER_HOT) {
CounterValues[COUNTER_HOT] = counter_hot_value->value();
}
}
void writeFile(int counterId) {
if (counterId == COUNTER_COLD) {
counter_cold_value->value() = CounterValues[COUNTER_COLD];
}else if (counterId == COUNTER_HOT) {
counter_hot_value->value() = CounterValues[COUNTER_HOT];
}
ESP_LOGD(TAG, "Saving done.");
}
void sendCounterValue(int counterId) {
if (counterId == COUNTER_COLD) {
ESP_LOGD(TAG, "Publishing cold state.");
float value = CounterValues[counterId];
cold_sensor->publish_state(value);
}else if (counterId == COUNTER_HOT) {
ESP_LOGD(TAG, "Publishing hot state.");
float value = CounterValues[counterId];
hot_sensor->publish_state(value);
}
}
String countrName(int Id) {
switch(Id) {
case COUNTER_HOT:
return "hot";
case COUNTER_COLD:
return "cold";
}
return "unknown";
}
void printState(int counterId) {
ESP_LOGD(TAG, "Counter[%d]=%.2f", counterId, CounterValues[counterId]);
}
};
+15 -57
View File
@@ -2,10 +2,6 @@
substitutions: substitutions:
devicename: watercontrol devicename: watercontrol
upper_devicename: Watercontrol upper_devicename: Watercontrol
pin_cold: D1
init_cold: '216.08'
pin_hot: D2
init_hot: '112.3'
water_open_pin: D5 water_open_pin: D5
water_close_pin: D3 water_close_pin: D3
ultrasonic_echo_pin: D6 ultrasonic_echo_pin: D6
@@ -19,6 +15,10 @@ esphome:
name: $devicename name: $devicename
platform: ESP8266 platform: ESP8266
board: d1_mini board: d1_mini
libraries:
- "Bounce2"
includes:
- water_counters.h
wifi: wifi:
ssid: !secret wifi_24_name ssid: !secret wifi_24_name
@@ -43,58 +43,25 @@ globals:
type: bool type: bool
restore_value: yes restore_value: yes
initial_value: 'true' initial_value: 'true'
- id: counter_cold_value
type: float
restore_value: yes
initial_value: $init_cold
- id: counter_hot_value
type: float
restore_value: yes
initial_value: $init_hot
binary_sensor:
- platform: gpio
id: pulse_cold
pin:
number: $pin_cold
mode: INPUT_PULLUP
filters:
- delayed_on: 1s
- delayed_off: 1s
on_press:
then:
- lambda: 'id(counter_cold_value) += 0.01;'
- platform: gpio
id: pulse_hot
pin:
number: $pin_hot
mode: INPUT_PULLUP
filters:
- delayed_on: 1s
- delayed_off: 1s
on_press:
then:
- lambda: 'id(counter_hot_value) += 0.01;'
#------------------------------------------------------------------------------
sensor: sensor:
- platform: uptime - platform: uptime
name: "$upper_devicename Uptime" name: "$upper_devicename Uptime"
- platform: template #------------------------------------------------------------------------------
name: "counter_hot" - platform: custom
accuracy_decimals: 2
lambda: |- lambda: |-
return id(counter_hot_value); auto my_sensor = new WaterCountersSensor();
update_interval: 60s App.register_component(my_sensor);
return {my_sensor->cold_sensor, my_sensor->hot_sensor};
- platform: template sensors:
name: "counter_cold" - name: "Counter Cold"
accuracy_decimals: 2
- name: "Counter Hot"
accuracy_decimals: 2 accuracy_decimals: 2
lambda: |-
return id(counter_cold_value);
update_interval: 60s
#------------------------------------------------------------------------------
- platform: ultrasonic - platform: ultrasonic
trigger_pin: $ultrasonic_trig_pin trigger_pin: $ultrasonic_trig_pin
echo_pin: $ultrasonic_echo_pin echo_pin: $ultrasonic_echo_pin
@@ -131,12 +98,3 @@ switch:
- delay: 5s - delay: 5s
- switch.turn_off: water_close - switch.turn_off: water_close
- lambda: 'id(water_state) = false;' - lambda: 'id(water_state) = false;'
- platform: template
name: "Reset"
turn_on_action:
- lambda: 'id(counter_cold_value) = 216.08;'
- lambda: 'id(counter_hot_value) = 112.3;'
turn_off_action:
- lambda: 'id(counter_cold_value) = 216.08;'
- lambda: 'id(counter_hot_value) = 112.3;'
+2 -4
View File
@@ -11,8 +11,6 @@ homeassistant:
logger: logger:
default: info default: info
logs:
custom_components.yandex_smart_home: debug
api: api:
@@ -68,7 +66,7 @@ remote: !include_dir_merge_list configuration/remote
#one item #one item
mqtt: !include configuration/mqtt/mqtt.yaml mqtt: !include configuration/mqtt/mqtt.yaml
telegram_bot: !include configuration/bot/telegram_bot.yaml telegram_bot: !include configuration/bot/telegram_bot.yaml
vacuum: !include configuration/xiaomi_aqara/vacuum.yaml # vacuum: !include configuration/xiaomi_aqara/vacuum.yaml
xiaomi_aqara: !include configuration/xiaomi_aqara/xiaomi_aqara.yaml xiaomi_aqara: !include configuration/xiaomi_aqara/xiaomi_aqara.yaml
nest: !include configuration/nest/nest.yaml nest: !include configuration/nest/nest.yaml
homekit: !include configuration/homekit/homekit.yaml homekit: !include configuration/homekit/homekit.yaml
@@ -81,4 +79,4 @@ modbus: !include configuration/modbus.yaml
utility_meter: !include_dir_merge_named configuration/utility_meter utility_meter: !include_dir_merge_named configuration/utility_meter
speedtestdotnet: !include configuration/speedtest.yaml speedtestdotnet: !include configuration/speedtest.yaml
yandex_smart_home: !include configuration/alice.yaml # yandex_smart_home: !include configuration/alice.yaml
-115
View File
@@ -1,115 +0,0 @@
filter:
include_domains:
- climate
- switch
- light
- vacuum
- cover
- fan
include_entities:
- input_boolean.alice_night_mode
- input_boolean.alice_turn_off_light
- input_boolean.alice_away_mode
exclude_entities:
- switch.plug_158d00020f3a29
- switch.plug_158d00026ea017
- switch.plug_158d00010dd98d
- switch.xiaomi_miio_switch
- switch.domofon_mode
- switch.domofon_open
- light.gateway_light_f0b429aa1463
- switch.domofon_automatically_open
- switch.domofon_automatically_open_once
- switch.domofon_force_open
- switch.wled_sync_receive
- switch.wled_sync_send
- switch.airfresher_fresh
- switch.airfresher_reset
entity_config:
input_boolean.alice_away_mode:
name: "Я ушёл"
room: "Сценарии"
input_boolean.alice_night_mode:
name: "Спокойной ночи"
room: "Сценарии"
input_boolean.alice_turn_off_light:
name: "Выключи свет"
room: "Сценарии"
climate.living_room:
name: "Термостат"
room: "Гостиная"
light.yeelight_mono1_f0b429a8f6df:
name: "Люстра"
room: "Гостиная"
light.yeelight_color1_34ce008fe328:
name: "Люстра"
room: "Гостиная"
light.yeelight_color1_286c071065c7:
name: "Люстра"
room: "Гостиная"
light.yeelight_mono1_f0b4290e5444:
name: "Люстра"
room: "Гостиная"
light.yeelight_mono1_34ce0087ff33:
name: "Люстра"
room: "Гостиная"
light.salt_lamp:
name: "Соляная лампа"
room: "Гостиная"
switch.wifi_socket:
name: "Гирлянда"
room: "Гостиная"
fan.xiaomi_miio_device:
name: "Воздухоочиститель"
room: "Гостиная"
switch.plug_158d0001104a0c:
name: "Телевизор"
room: "Гостиная"
cover.curtain_158d0002b0c46a:
name: "Шторы"
room: "Гостиная"
light.bath:
name: "Люстра"
room: "Туалет"
light.philips_led:
name: "Люстра"
room: "Туалет"
light.kitchen_mini_light:
name: "Подсветка"
room: "Кухня"
light.kitchen:
name: "Люстра"
room: "Кухня"
switch.plug_158d0001a68c64:
name: "Посудомойка"
room: "Кухня"
switch.plug_158d0001a4cfb4:
name: "Стиралка"
room: "Кухня"
light.hall:
name: "Люстра"
room: "Коридор"
switch.plug_158d0001a68b18:
name: "Термопот"
room: "Кухня"
light.yeelight_mono1_f0b429a8d936:
name: "Люстра"
room: "Балкон"
light.yeelight_mono1_286c07f12bed:
name: "Люстра"
room: "Балкон"
switch.plug_158d0002924779:
name: "Фитолампа"
room: "Балкон"
vacuum.xiaomi_vacuum_cleaner:
name: "Пылесос"
room: "Дом"
switch.water_valve:
name: "Подача воды"
room: "Дом"
light.wled:
name: "Ёлка"
room: "Гостиная"
@@ -1,4 +0,0 @@
- name: appletv4
host: !secret atv4_ip
login_id: !secret atv4_login
credentials: !secret atv4_credentials
@@ -1,6 +0,0 @@
- platform: mqtt
state_topic: "home/magnet/balcony/status"
name: "door_window_sensor_158d00010f7e19"
payload_on: "open"
payload_off: "close"
device_class: opening
@@ -1,6 +0,0 @@
- platform: mqtt
state_topic: "home/magnet/bath/status"
name: "door_window_sensor_158d000111a469"
payload_on: "open"
payload_off: "close"
device_class: opening
@@ -1,6 +0,0 @@
- platform: mqtt
state_topic: "home/magnet/hall/status"
name: "door_window_sensor_158d0001130141"
payload_on: "open"
payload_off: "close"
device_class: opening
@@ -1,18 +0,0 @@
- platform: mqtt
state_topic: "home/motion/bath/status"
name: "motion_sensor_158d0000f9546b" #"bath_motion"
payload_on: "motion"
payload_off: "no_motion"
device_class: motion
- platform: mqtt
state_topic: "home/sensor_motion.aq2/bath2/status"
name: "motion_sensor_158d000165fed5" #"bath_motion2"
payload_on: "motion"
payload_off: "no_motion"
device_class: motion
- platform: mqtt
state_topic: "home/motion/bath3/status"
name: "motion_sensor_158d0001537e21" #"bath_motion3"
payload_on: "motion"
payload_off: "no_motion"
device_class: motion
@@ -1,6 +0,0 @@
- platform: mqtt
state_topic: "home/motion/hall/status"
name: "motion_sensor_158d0000e7c7ad"
payload_on: "motion"
payload_off: "no_motion"
device_class: motion
@@ -1,12 +0,0 @@
- platform: mqtt
state_topic: "home/motion/kitchen/status"
name: "motion_sensor_158d0000fbc664"
payload_on: "motion"
payload_off: "no_motion"
device_class: motion
- platform: mqtt
state_topic: "home/motion/kitchen2/status"
name: "motion_sensor_158d0001535c97"
payload_on: "motion"
payload_off: "no_motion"
device_class: motion
@@ -1,6 +0,0 @@
- platform: mqtt
state_topic: "home/motion/mainroom/status"
name: "motion_sensor_158d0000f6b2fa"
payload_on: "motion"
payload_off: "no_motion"
device_class: motion
@@ -1,6 +0,0 @@
- platform: mqtt
state_topic: "home/sensor_wleak.aq1/kitchen/status"
name: "water_leak_sensor_158d0001d594fa"
payload_on: "leak"
payload_off: "no_leak"
device_class: moisture
@@ -1,4 +0,0 @@
- platform: xiaomi_miio
model: zhimi.airpurifier.mc1
host: !secret purifier_ip
token: !secret purifier_key
@@ -2,6 +2,5 @@ bath_motion:
name: Движение (ванна) name: Движение (ванна)
control: hidden control: hidden
entities: entities:
- binary_sensor.motion_sensor_158d0000f9546b
- binary_sensor.motion_sensor_158d000165fed5 - binary_sensor.motion_sensor_158d000165fed5
- binary_sensor.motion_sensor_158d0001537e21 - binary_sensor.motion_sensor_158d0001537e21
@@ -2,5 +2,4 @@ hall_motion:
name: Движение (прихожая) name: Движение (прихожая)
control: hidden control: hidden
entities: entities:
- binary_sensor.motion_sensor_158d0000e7c7ad
- binary_sensor.motion_sensor_158d0002044548 - binary_sensor.motion_sensor_158d0002044548
@@ -2,5 +2,4 @@ kitchen_motion:
name: Движение (кухня) name: Движение (кухня)
control: hidden control: hidden
entities: entities:
- binary_sensor.motion_sensor_158d0000fbc664
- binary_sensor.motion_sensor_158d0001535c97 - binary_sensor.motion_sensor_158d0001535c97
@@ -3,5 +3,4 @@ mainroom_motion:
control: hidden control: hidden
entities: entities:
- binary_sensor.motion_sensor_158d0000f6b2fa - binary_sensor.motion_sensor_158d0000f6b2fa
- binary_sensor.motion_sensor_158d0001e50ce4
- binary_sensor.motion_sensor_158d0001e80f0d - binary_sensor.motion_sensor_158d0001e80f0d
@@ -1,7 +0,0 @@
# - platform: mqtt
# name: "balcony"
# state_topic: "stat/BALCONYV2/POWER"
# command_topic: "cmnd/BALCONYV2/POWER"
# payload_on: "ON"
# payload_off: "OFF"
# optimistic: false
@@ -1,13 +0,0 @@
- platform: mqtt
name: "salt_lamp"
availability_topic: "home/noolite/$online"
payload_available: "true"
payload_not_available: "false"
state_topic: "home/noolite/ch0/power"
command_topic: "home/noolite/ch0/power/set"
on_command_type: first
payload_on: "on"
payload_off: "off"
brightness_state_topic: "home/noolite/ch0/brightness"
brightness_command_topic: "home/noolite/ch0/brightness/set"
brightness_scale: 120
@@ -1,4 +0,0 @@
#gateways:
# - device: !secret mysensors_gateway_ip
# tcp_port: 5003
#version: '2.0'
@@ -1,4 +0,0 @@
# - platform: xiaomi_miio
# name: xiaomi_remote
# host: !secret xiaomi_remote_host
# token: !secret xiaomi_remote_token
@@ -1,6 +0,0 @@
airfresher_reset:
sequence:
- service: mqtt.publish
data:
topic: 'home/airfresher/airfresher/remain/set'
payload: 1500
@@ -1,6 +0,0 @@
audio_bass_minus:
sequence:
- service: mqtt.publish
data:
topic: "home/remote/rm2/code/set"
payload: "260050000001339514371412131313131312131313131313131313381337133814371337143714371412133814361437141213131213131313381412131313131238143714371436140005060001334a14000d050000000000000000"
@@ -1,6 +0,0 @@
audio_bass_plus:
sequence:
- service: mqtt.publish
data:
topic: "home/remote/rm2/code/set"
payload: "260050000001339414371412131313131213131313131313131313381436143714371437143713381412133813121338141213131313131313381312133814121338143614371437140005060001324a14000d050000000000000000"
@@ -1,6 +0,0 @@
audio_vol_minus:
sequence:
- service: mqtt.publish
data:
topic: "home/remote/rm2/code/set"
payload: "260050000001329513381313131313121313131313131313131312381338133813371338133813381313121313131313133812131313131313381338123813381313123813381338120005060001314b13000d050000000000000000"
@@ -1,6 +0,0 @@
audio_vol_plus:
sequence:
- service: mqtt.publish
data:
topic: "home/remote/rm2/code/set"
payload: "260058000001309613381213131313131213141214121313141113381437143614371437133714371437141213121313143713121313131313131238143713381313123814371436150005030001324916000c5b0001324914000d05"
@@ -1,7 +0,0 @@
- platform: template
sensors:
xiaomi_airpurifier_air_quality_pm25:
friendly_name: "Air quality"
value_template: "{{ state_attr('fan.xiaomi_miio_device', 'aqi') }}"
unit_of_measurement: "μg/m³"
icon_template: "mdi:weather-fog"
@@ -1,12 +0,0 @@
# - platform: mqtt
# name: kitchen_mini_light
# state_topic: "home/plug/kitchen-mini-light/status"
# command_topic: "home/plug/kitchen-mini-light/status/set"
# payload_on: "on"
# payload_off: "off"
# - platform: broadlink
# host: 192.168.1.63
# mac: 'B4430DCB6C07'
# type: 'sp2'
# friendly_name: 'kitchen_mini_light'
@@ -1,3 +0,0 @@
- platform: xiaomi_miio
host: !secret mirobot_ip
token: !secret mirobot_key
@@ -1,187 +0,0 @@
"""
Custom element manager for community created elements.
For more details about this integration, please refer to the documentation at
https://hacs.xyz/
"""
import voluptuous as vol
from aiogithubapi import AIOGitHub
from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.const import __version__ as HAVERSION
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.event import async_call_later
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
from .const import DEV_MODE, DOMAIN, ELEMENT_TYPES, STARTUP, VERSION
from .constrains import check_constans
from .hacsbase import Hacs
from .hacsbase.configuration import Configuration
from .hacsbase.data import HacsData
from .hacsbase.migration import ValidateData
from .setup import add_sensor, load_hacs_repository, setup_frontend
SCHEMA = hacs_base_config_schema()
SCHEMA[vol.Optional("options")] = hacs_config_option_schema()
CONFIG_SCHEMA = vol.Schema({DOMAIN: SCHEMA}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up this integration using yaml."""
if DOMAIN not in config:
return True
hass.data[DOMAIN] = config
Hacs.hass = hass
Hacs.configuration = Configuration.from_dict(
config[DOMAIN], config[DOMAIN].get("options")
)
Hacs.configuration.config_type = "yaml"
await startup_wrapper_for_yaml(Hacs)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={}
)
)
return True
async def async_setup_entry(hass, config_entry):
"""Set up this integration using UI."""
conf = hass.data.get(DOMAIN)
if config_entry.source == config_entries.SOURCE_IMPORT:
if conf is None:
hass.async_create_task(
hass.config_entries.async_remove(config_entry.entry_id)
)
return False
Hacs.hass = hass
Hacs.configuration = Configuration.from_dict(
config_entry.data, config_entry.options
)
Hacs.configuration.config_type = "flow"
Hacs.configuration.config_entry = config_entry
config_entry.add_update_listener(reload_hacs)
startup_result = await hacs_startup(Hacs)
if not startup_result:
raise ConfigEntryNotReady
return startup_result
async def startup_wrapper_for_yaml(hacs):
"""Startup wrapper for yaml config."""
startup_result = await hacs_startup(hacs)
if not startup_result:
hacs.hass.components.frontend.async_remove_panel(
hacs.configuration.sidepanel_title.lower()
.replace(" ", "_")
.replace("-", "_")
)
hacs.logger.info("Could not setup HACS, trying again in 15 min")
async_call_later(hacs.hass, 900, startup_wrapper_for_yaml(hacs))
async def hacs_startup(hacs):
"""HACS startup tasks."""
hacs.logger.debug(f"Configuration type: {hacs.configuration.config_type}")
hacs.version = VERSION
hacs.logger.info(STARTUP)
hacs.system.config_path = hacs.hass.config.path()
hacs.system.ha_version = HAVERSION
hacs.system.disabled = False
hacs.github = AIOGitHub(
hacs.configuration.token, async_create_clientsession(hacs.hass)
)
hacs.data = HacsData()
# Check HACS Constrains
if not check_constans(hacs):
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
# Set up frontend
await setup_frontend(hacs)
# Load HACS
if not await load_hacs_repository(hacs):
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
val = ValidateData()
if not val.validate_local_data_file():
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
# Restore from storefiles
if not await hacs.data.restore():
hacs_repo = hacs().get_by_name("hacs/integration")
hacs_repo.pending_restart = True
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
# Add aditional categories
if hacs.configuration.appdaemon:
ELEMENT_TYPES.append("appdaemon")
if hacs.configuration.python_script:
ELEMENT_TYPES.append("python_script")
if hacs.configuration.theme:
ELEMENT_TYPES.append("theme")
hacs.common.categories = sorted(ELEMENT_TYPES)
# Setup startup tasks
if hacs.configuration.config_type == "yaml":
hacs.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, hacs().startup_tasks()
)
else:
async_call_later(hacs.hass, 5, hacs().startup_tasks())
# Print DEV warning
if hacs.configuration.dev:
hacs.logger.warning(DEV_MODE)
hacs.hass.components.persistent_notification.create(
title="HACS DEV MODE", message=DEV_MODE, notification_id="hacs_dev_mode"
)
# Add sensor
add_sensor(hacs)
# Set up services
# await add_services(hacs)
# Mischief managed!
return True
async def async_remove_entry(hass, config_entry):
"""Handle removal of an entry."""
Hacs().logger.info("Disabling HACS")
Hacs().logger.info("Removing recuring tasks")
for task in Hacs().tasks:
task()
Hacs().logger.info("Removing sensor")
try:
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
except ValueError:
pass
Hacs().logger.info("Removing sidepanel")
try:
hass.components.frontend.async_remove_panel("hacs_web")
except AttributeError:
pass
Hacs().system.disabled = True
Hacs().logger.info("HACS is now disabled")
async def reload_hacs(hass, config_entry):
"""Reload HACS."""
await async_remove_entry(hass, config_entry)
await async_setup_entry(hass, config_entry)
-421
View File
@@ -1,421 +0,0 @@
"""API Endpoins."""
from time import time
from aiohttp import web
from integrationhelper import Logger
from .http import HacsWebResponse
APIRESPONSE = {}
def apiresponse(classname):
"""Decorator used to register API Responses."""
APIRESPONSE[classname.name] = classname
return classname
class HacsAPI(HacsWebResponse):
"""HacsAPI class."""
name = "hacsapi"
def __init__(self):
"""Initialize."""
self.logger = Logger("hacs.api")
self.url = self.hacsapi + "/{endpoint}"
async def post(self, request, endpoint): # pylint: disable=unused-argument
"""Handle HACS API requests."""
if self.system.disabled:
return web.Response(status=404)
self.endpoint = endpoint
self.postdata = await request.post()
self.raw_headers = request.raw_headers
self.request = request
self.logger.debug(f"Endpoint ({endpoint}) called")
if self.configuration.dev:
self.logger.debug(f"Raw headers ({self.raw_headers})")
self.logger.debug(f"Postdata ({self.postdata})")
if self.endpoint in APIRESPONSE:
try:
response = APIRESPONSE[self.endpoint]
response = await response.response(self)
except Exception as exception:
render = self.render(f"error", message=exception)
return web.Response(
body=render, content_type="text/html", charset="utf-8"
)
else:
# Return default response.
response = await APIRESPONSE["generic"].response(self)
# set headers
response.headers["Cache-Control"] = "max-age=0, must-revalidate"
# serve the response
return response
class HacsRunningTask(HacsAPI):
"""Return if BG task is running."""
name = "hacs:task"
def __init__(self):
"""Initialize."""
self.url = "/hacs_task"
async def get(self, request): # pylint: disable=unused-argument
"""Handle GET request."""
return web.json_response({"task": self.system.status.background_task})
@apiresponse
class Generic(HacsAPI):
"""Generic API response."""
name = "generic"
async def response(self):
"""Response."""
self.logger.error(f"Unknown endpoint '{self.endpoint}'")
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RemoveNewFlag(HacsAPI):
"""Remove new flag on all repositories."""
name = "remove_new_flag"
async def response(self):
"""Response."""
for repository in self.repositories:
repository.status.new = False
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/{self.postdata['return']}?timestamp={time()}"
)
@apiresponse
class DevTemplate(HacsAPI):
"""Remove new flag on all repositories."""
name = "dev_template"
async def response(self):
"""Response."""
from .handler.template import render_template
if "set" in self.postdata:
self.developer.template_id = self.postdata.get("repository_id")
repository = self.get_by_id(self.developer.template_id)
template = render_template(self.postdata.get("template", ""), repository)
info = await self.github.render_markdown(template)
info = info.replace("<h3>", "<h6>").replace("</h3>", "</h6>")
info = info.replace("<h2>", "<h5>").replace("</h2>", "</h5>")
info = info.replace("<h1>", "<h4>").replace("</h1>", "</h4>")
info = info.replace("<code>", "<code class='codeinfo'>")
info = info.replace(
'<a href="http', '<a rel="noreferrer" target="_blank" href="http'
)
info = info.replace("<ul>", "")
info = info.replace("</ul>", "")
self.developer.template_content = info
else:
self.developer.template_content = ""
self.developer.template_id = "Repository ID"
render = self.render("settings/dev/template_test")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@apiresponse
class DevView(HacsAPI):
"""Set HA version view."""
name = "devview"
async def response(self):
"""Response."""
render = self.render(f"settings/dev/{self.postdata['view']}")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@apiresponse
class SetHAVersionAction(HacsAPI):
"""Set HA version action."""
name = "set_ha_version_action"
async def response(self):
"""Response."""
self.common.ha_version = self.postdata["ha_version"]
render = self.render("settings/dev/set_ha_version")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@apiresponse
class RepositoryInstall(HacsAPI):
"""Install repository."""
name = "repository_install"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.install()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoryUpdate(HacsAPI):
"""Update repository."""
name = "repository_update"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.update_repository()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoryUninstall(HacsAPI):
"""Uninstall repository."""
name = "repository_uninstall"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.uninstall()
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/overview?timestamp={time()}")
@apiresponse
class RepositoryRemove(HacsAPI):
"""Remove repository."""
name = "repository_remove"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.remove()
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoryHide(HacsAPI):
"""Hide repository."""
name = "repository_hide"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.hide = True
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/store?timestamp={time()}")
@apiresponse
class RepositoryUnhide(HacsAPI):
"""Unhide repository."""
name = "repository_unhide"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.hide = False
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoryBetaHide(HacsAPI):
"""Hide Beta repository."""
name = "repository_beta_hide"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.status.show_beta = False
await repository.update_repository()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoryBetaShow(HacsAPI):
"""Show Beta repository."""
name = "repository_beta_show"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.status.show_beta = True
await repository.update_repository()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoriesReload(HacsAPI):
"""Reload repository data."""
name = "repositories_reload"
async def response(self):
"""Response."""
self.hass.async_create_task(self.recuring_tasks_all())
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoriesUpgradeAll(HacsAPI):
"""Upgrade all repositories."""
name = "repositories_upgrade_all"
async def response(self):
"""Response."""
for repository in self.repositories:
if repository.pending_upgrade:
await repository.install()
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoryRegister(HacsAPI):
"""Register repository."""
name = "repository_register"
async def response(self):
"""Response."""
repository_name = self.postdata.get("custom_url")
repository_type = self.postdata.get("repository_type")
# Validate data
if not repository_name:
message = "Repository URL is missing."
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
if repository_type is None:
message = "Type is missing for '{}'.".format(repository_name)
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
# Stip first part if it's an URL.
if "github" in repository_name:
repository_name = repository_name.split("github.com/")[-1]
# Strip whitespace
repository_name = repository_name.split()[0]
# If it still have content, continue.
if repository_name != "":
if len(repository_name.split("/")) != 2:
message = f"""
{repository_name} is not a valid format.
Correct format is 'https://github.com/DEVELOPER/REPOSITORY'
or 'DEVELOPER/REPOSITORY'.
"""
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
is_known_repository = self.is_known(repository_name)
if is_known_repository:
message = f"'{repository_name}' is already registered, look for it in the store."
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
if repository_name in self.common.blacklist:
self.common.blacklist.remove(repository_name)
await self.register_repository(repository_name, repository_type)
repository = self.get_by_name(repository_name)
if repository is not None:
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
message = f"""
Could not add '{repository_name}' with type '{repository_type}' at this time.</br>
If you used the correct type, check the log for more details."""
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
@apiresponse
class RepositorySelectTag(HacsAPI):
"""Select tag for Repository."""
name = "repository_select_tag"
async def response(self):
"""Response."""
from aiogithubapi import AIOGitHubException
from .hacsbase.exceptions import HacsRequirement
repository = self.get_by_id(self.postdata["repository_id"])
if self.postdata["selected_tag"] == repository.releases.last_release:
repository.status.selected_tag = None
else:
repository.status.selected_tag = self.postdata["selected_tag"]
try:
await repository.update_repository()
except (AIOGitHubException, HacsRequirement):
repository.status.selected_tag = repository.releases.last_release
await repository.update_repository()
message = "The version {} is not valid for use with HACS.".format(
self.postdata["selected_tag"]
)
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}&message={message}"
)
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class FrontentMode(HacsAPI):
"""Set the frontend mode."""
name = "frontend_mode"
async def response(self):
"""Response."""
self.configuration.frontend_mode = self.postdata["view_type"]
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@@ -1,100 +0,0 @@
"""Adds config flow for HACS."""
# pylint: disable=dangerous-default-value
import logging
import voluptuous as vol
from aiogithubapi import AIOGitHub, AIOGitHubException, AIOGitHubAuthentication
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
_LOGGER = logging.getLogger(__name__)
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for HACS."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize."""
self._errors = {}
async def async_step_user(self, user_input={}):
"""Handle a flow initialized by the user."""
self._errors = {}
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if self.hass.data.get(DOMAIN):
return self.async_abort(reason="single_instance_allowed")
if user_input is not None:
if await self._test_token(user_input["token"]):
return self.async_create_entry(title="", data=user_input)
self._errors["base"] = "auth"
return await self._show_config_form(user_input)
return await self._show_config_form(user_input)
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(hacs_base_config_schema(user_input)),
errors=self._errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return HacsOptionsFlowHandler(config_entry)
async def async_step_import(self, user_input):
"""Import a config entry.
Special type of import, we're not actually going to store any data.
Instead, we're going to rely on the values that are in config file.
"""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="configuration.yaml", data={})
async def _test_token(self, token):
"""Return true if token is valid."""
try:
session = aiohttp_client.async_get_clientsession(self.hass)
client = AIOGitHub(token, session)
await client.get_repo("hacs/org")
return True
except (AIOGitHubException, AIOGitHubAuthentication):
pass
return False
class HacsOptionsFlowHandler(config_entries.OptionsFlow):
"""HACS config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
hacs_config_option_schema(self.config_entry.options)
),
)
@@ -1,49 +0,0 @@
"""HACS Configuration Schemas."""
# pylint: disable=dangerous-default-value
import voluptuous as vol
from .const import LOCALE
# Configuration:
TOKEN = "token"
SIDEPANEL_TITLE = "sidepanel_title"
SIDEPANEL_ICON = "sidepanel_icon"
APPDAEMON = "appdaemon"
PYTHON_SCRIPT = "python_script"
THEME = "theme"
# Options:
COUNTRY = "country"
RELEASE_LIMIT = "release_limit"
EXPERIMENTAL = "experimental"
def hacs_base_config_schema(config: dict = {}) -> dict:
"""Return a shcema configuration dict for HACS."""
if not config:
config = {
TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
SIDEPANEL_TITLE: "Community",
SIDEPANEL_ICON: "mdi:alpha-c-box",
APPDAEMON: False,
PYTHON_SCRIPT: False,
THEME: False,
}
return {
vol.Required(TOKEN, default=config.get(TOKEN)): str,
vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str,
vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str,
vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool,
vol.Optional(PYTHON_SCRIPT, default=config.get(PYTHON_SCRIPT)): bool,
vol.Optional(THEME, default=config.get(THEME)): bool,
}
def hacs_config_option_schema(options: dict = {}) -> dict:
"""Return a shcema for HACS configuration options."""
if not options:
options = {COUNTRY: "ALL", RELEASE_LIMIT: 5, EXPERIMENTAL: False}
return {
vol.Optional("country", default=options.get(COUNTRY)): vol.In(LOCALE),
vol.Optional(RELEASE_LIMIT, default=options.get(RELEASE_LIMIT)): int,
vol.Optional(EXPERIMENTAL, default=options.get(EXPERIMENTAL)): bool,
}
@@ -1,295 +0,0 @@
"""Constants for HACS"""
NAME_LONG = "HACS (Home Assistant Community Store)"
NAME_SHORT = "HACS"
VERSION = "0.16.3"
DOMAIN = "hacs"
PROJECT_URL = "https://github.com/hacs/integration/"
CUSTOM_UPDATER_LOCATIONS = [
"{}/custom_components/custom_updater.py",
"{}/custom_components/custom_updater/__init__.py",
]
ISSUE_URL = f"{PROJECT_URL}issues"
DOMAIN_DATA = f"{NAME_SHORT.lower()}_data"
ELEMENT_TYPES = ["integration", "plugin"]
IFRAME = {
"title": "Community",
"icon": "mdi:alpha-c-box",
"url": "/community_overview",
"path": "community",
"require_admin": True,
}
# Messages
CUSTOM_UPDATER_WARNING = """
This cannot be used with custom_updater.
To use this you need to remove custom_updater form {}
"""
DEV_MODE = "You have 'dev' enabled for HACS, this is not intended for regular use, no support will be given if you break something."
STARTUP = f"""
-------------------------------------------------------------------
HACS (Home Assistant Community Store)
Version: {VERSION}
This is a custom integration
If you have any issues with this you need to open an issue here:
https://github.com/hacs/integration/issues
-------------------------------------------------------------------
"""
LOCALE = [
"ALL",
"AF",
"AL",
"DZ",
"AS",
"AD",
"AO",
"AI",
"AQ",
"AG",
"AR",
"AM",
"AW",
"AU",
"AT",
"AZ",
"BS",
"BH",
"BD",
"BB",
"BY",
"BE",
"BZ",
"BJ",
"BM",
"BT",
"BO",
"BQ",
"BA",
"BW",
"BV",
"BR",
"IO",
"BN",
"BG",
"BF",
"BI",
"KH",
"CM",
"CA",
"CV",
"KY",
"CF",
"TD",
"CL",
"CN",
"CX",
"CC",
"CO",
"KM",
"CG",
"CD",
"CK",
"CR",
"HR",
"CU",
"CW",
"CY",
"CZ",
"CI",
"DK",
"DJ",
"DM",
"DO",
"EC",
"EG",
"SV",
"GQ",
"ER",
"EE",
"ET",
"FK",
"FO",
"FJ",
"FI",
"FR",
"GF",
"PF",
"TF",
"GA",
"GM",
"GE",
"DE",
"GH",
"GI",
"GR",
"GL",
"GD",
"GP",
"GU",
"GT",
"GG",
"GN",
"GW",
"GY",
"HT",
"HM",
"VA",
"HN",
"HK",
"HU",
"IS",
"IN",
"ID",
"IR",
"IQ",
"IE",
"IM",
"IL",
"IT",
"JM",
"JP",
"JE",
"JO",
"KZ",
"KE",
"KI",
"KP",
"KR",
"KW",
"KG",
"LA",
"LV",
"LB",
"LS",
"LR",
"LY",
"LI",
"LT",
"LU",
"MO",
"MK",
"MG",
"MW",
"MY",
"MV",
"ML",
"MT",
"MH",
"MQ",
"MR",
"MU",
"YT",
"MX",
"FM",
"MD",
"MC",
"MN",
"ME",
"MS",
"MA",
"MZ",
"MM",
"NA",
"NR",
"NP",
"NL",
"NC",
"NZ",
"NI",
"NE",
"NG",
"NU",
"NF",
"MP",
"NO",
"OM",
"PK",
"PW",
"PS",
"PA",
"PG",
"PY",
"PE",
"PH",
"PN",
"PL",
"PT",
"PR",
"QA",
"RO",
"RU",
"RW",
"RE",
"BL",
"SH",
"KN",
"LC",
"MF",
"PM",
"VC",
"WS",
"SM",
"ST",
"SA",
"SN",
"RS",
"SC",
"SL",
"SG",
"SX",
"SK",
"SI",
"SB",
"SO",
"ZA",
"GS",
"SS",
"ES",
"LK",
"SD",
"SR",
"SJ",
"SZ",
"SE",
"CH",
"SY",
"TW",
"TJ",
"TZ",
"TH",
"TL",
"TG",
"TK",
"TO",
"TT",
"TN",
"TR",
"TM",
"TC",
"TV",
"UG",
"UA",
"AE",
"GB",
"US",
"UM",
"UY",
"UZ",
"VU",
"VE",
"VN",
"VG",
"VI",
"WF",
"EH",
"YE",
"ZM",
"ZW",
]
@@ -1,44 +0,0 @@
"""HACS Startup constrains."""
# pylint: disable=bad-continuation
import os
import json
from .const import CUSTOM_UPDATER_LOCATIONS, CUSTOM_UPDATER_WARNING
from .helpers.misc import version_is_newer_than_version
def check_constans(hacs):
"""Check HACS constrains."""
if not constrain_custom_updater(hacs):
return False
if not constrain_version(hacs):
return False
return True
def constrain_custom_updater(hacs):
"""Check if custom_updater exist."""
for location in CUSTOM_UPDATER_LOCATIONS:
if os.path.exists(location.format(hacs.system.config_path)):
msg = CUSTOM_UPDATER_WARNING.format(
location.format(hacs.system.config_path)
)
hacs.logger.critical(msg)
return False
return True
def constrain_version(hacs):
"""Check if the version is valid."""
with open(
f"{hacs.system.config_path}/custom_components/hacs/manifest.json", "r"
) as read:
manifest = json.loads(read.read())
# Check if HA is the required version.
if version_is_newer_than_version(manifest["homeassistant"], hacs.system.ha_version):
hacs.logger.critical(
f"You need HA version {manifest['homeassistant']} or newer to use this integration."
)
return False
return True
@@ -1,49 +0,0 @@
"""Deprecated functions, should be fine to remove."""
async def add_services(hacs):
"""Add services."""
# Service registration
async def service_hacs_install(call):
"""Install a repository."""
repository = str(call.data["repository"])
if repository not in hacs().store.repositories:
hacs.logger.error("%s is not a konwn repository!", repository)
return
repository = hacs().store.repositories[repository]
await repository.install()
async def service_hacs_register(call):
"""register a repository."""
repository = call.data["repository"]
repository_type = call.data["repository_type"]
if await hacs().is_known_repository(repository):
hacs.logger.error("%s is already a konwn repository!", repository)
return
await hacs().register_new_repository(repository_type, repository)
async def service_hacs_load(call):
"""register a repository."""
from homeassistant.loader import async_get_custom_components
del hacs.hass.data["custom_components"]
await async_get_custom_components(hacs.hass)
hacs.hass.services.async_register("hacs", "install", service_hacs_install)
hacs.hass.services.async_register("hacs", "register", service_hacs_register)
hacs.hass.services.async_register("hacs", "load", service_hacs_load)
# This should probably be in a dedicated test "package"
async def test_repositories(hacs):
"""Test repositories."""
await hacs().register_repository("ludeeus/theme-hacs", "theme")
await hacs().register_repository("ludeeus/ps-hacs", "python_script")
await hacs().register_repository("ludeeus/integration-hacs", "integration")
await hacs().register_repository(
"rgruebel/ha_zigbee2mqtt_networkmap", "integration"
)
await hacs().register_repository("ludeeus/ad-hacs", "appdaemon")
await hacs().register_repository("jonkristian/entur-card", "plugin") # Dist
await hacs().register_repository("kalkih/mini-media-player", "plugin") # Release
await hacs().register_repository("custom-cards/monster-card", "plugin") # root
@@ -1,504 +0,0 @@
.yaml {
font-family: monospace, monospace;
font-size: 1em;
border-style: solid;
border-width: thin;
margin: 0;
overflow: auto;
display: inline-flex;
width: calc(100% - 20px);
white-space: pre-wrap;
}
.hacslist {
border: none;
}
.hacs-card-container {
display: grid;
grid-gap: 10px;
}
.hacs-overview-container {
width: 95%;
margin-left: 2.5%;
margin-right: 2.5%;
margin-bottom: 10px;
display: inline-block;
}
.hacs-form {
display: contents;
}
.hacs-table {
background-color: var(--paper-card-background-color);
}
.hacs-content {
position: absolute;
top: 49px;
left: 0px;
right: 0px;
bottom: 0px;
overflow-y: auto;
overflow-x: hidden;
padding-top: 20px;
padding-bottom: 15px;
}
.hacs-card {
height: 100%;
background-color: var(--paper-card-background-color) !important;
padding: 3px;
line-height: 1;
}
.overview {
height: 100%;
}
.progressbar {
margin-left: 2.5%;
margin-right: 2.5%;
margin-bottom: 20px;
}
.hacs-card-title {
font-size: medium;
color: var(--primary-text-color);
}
tr.hacs-table-row.repository {
border-bottom: 0px;
display: table-row-group;
}
td.repository {
width: 50%;
padding: 0px;
}
td.repository.text {
padding-left: 12px;
}
select.hacs-select.repository {
width: 100%;
}
.hacs-card-content {
color: var(--secondary-text-color);
font-style: italic;
font-size: small;
}
.card-status {
font-size: 18px;
padding-right: 4px;
}
.hacs-card-standalone {
background-color: var(--paper-card-background-color) !important;
color: var(--primary-text-color);
padding: 16px;
}
.installed {
color: var(--hacs-status-installed);
}
.pending-restart {
color: var(--hacs-status-pending-restart);
}
.pending-upgrade {
color: var(--hacs-status-pending-update);
}
.default {
color: var(--primary-text-color);
}
.hacs-nav {
background-color: var(--primary-color) !important;
position: fixed;
height: auto;
}
a {
color: var(--primary-color);
}
.disabledaction {
cursor: default;
color: var(--primary-color);
opacity: 0.5;
}
.hacs-bar {
background-color: var(--primary-color) !important;
}
.hacs-bar-background {
background-color: var(--dark-primary-color) !important;
}
.errorview {
display: block;
margin-bottom: 30px;
}
.hacscolor {
background-color: var(--paper-card-background-color) !important;
color: var(--primary-text-color);
}
.hacsbutton {
margin: 3%;
background-color: var(--primary-color) !important;
}
.hacsadminbutton {
margin: 1%;
background-color: var(--primary-color) !important;
}
body {
background-color: var(--primary-background-color) !important;
color: var(--primary-text-color);
overflow: hidden;
font-size: 14px;
}
input[type="text"]:not(.browser-default):focus:not([readonly]) {
border-bottom: 1px solid var(--primary-color);
-webkit-box-shadow: 0 1px 0 0 var(--primary-color);
box-shadow: 0 1px 0 0 var(--primary-color);
}
.dropdown-list-item {
color: var(--primary-color) !important;
background-color: var(--paper-card-background-color) !important;
}
a.dropdown-list-item:hover {
opacity: 0.95;
}
.actionlink {
color: var(--primary-color) !important
}
.uninstall {
float: right;
color: var(--google-red-500) !important;
font-weight: bold;
}
@media all and (max-width: 550px) {
select.hacs-select.custom_repository {
width: 100%;
}
input#custom_url {
width: 100% !important;
}
.actionlink {
width: 100%;
padding: 5px;
display: block;
font-size: x-large;
text-align: center;
}
.uninstall {
float: none;
}
}
.minigrid {
display: contents;
}
.card-action a:hover {
opacity: 0.6 !important;
}
.codeinfo {
opacity: 0.6 !important;
}
.hacs-select {
display: initial !important;
margin-left: 1%;
width: 20%;
background-color: transparent;
border-color: transparent;
color: var(--primary-text-color);
}
.hacs-badge {
background-color: var(--hacs-badge-color);
margin-right: 1%;
font-size: 12px;
padding: 0px 4px 2px 4px;
color: var(--hacs-badge-text-color);
-moz-border-radius: 10px 10px 10px 10px;
border-radius: 10px 10px 10px 10px;
}
tr.hacs-table-row:hover {
cursor: pointer;
}
/* color vars*/
:root {
--hacs-status-installed: #126e15;
--hacs-status-pending-restart: #a70000;
--hacs-status-pending-update: #ffab40;
--hacs-status-default: var(--primary-text-color);
--hacs-badge-color: var(--primary-color);
--hacs-badge-text-color: var(--primary-text-color);
--primary-color: #03a9f4;
--primary-text-color: #000000;
--secondary-text-color: #424141;
--primary-background-color: #fafafa;
--paper-card-background-color: #ffffff;
--dark-primary-color: #00405d;
--google-red-500: #db4437;
}
/* hug screens*/
@media all and (min-width: 2000px) {
.hacs-card-container {
grid-template-columns: repeat(6, 1fr);
}
}
/* even bigger screens*/
@media all and (min-width: 1500px) and (max-width: 1999px) {
.hacs-card-container {
grid-template-columns: repeat(5, 1fr);
}
}
/* bigger screens*/
@media all and (min-width: 1000px) and (max-width: 1499px) {
.hacs-card-container {
grid-template-columns: repeat(4, 1fr);
}
}
/* big screens */
@media all and (min-width: 550px) and (max-width: 999px) {
.hacs-card-container {
grid-template-columns: repeat(3, 1fr);
}
#main::-webkit-scrollbar-track
{
border-radius: 10px;
}
#main::-webkit-scrollbar
{
width: 12px;
}
#main::-webkit-scrollbar-thumb
{
border-radius: 10px;
background-color: var(--primary-color);
}
}
/* smal screens */
@media all and (max-width: 550px) {
body {
overflow: scroll !important;
}
.minigrid {
display: grid;
}
.smal-hide {
display: none !important;
}
.hacs-content {
overflow: scroll !important;
-webkit-overflow-scrolling: touch;
}
}
/* laoder */
*.hidden {
display: none !important;
}
div.loading{
position: fixed;
z-index: 999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(16, 16, 16, 0.5);
}
@-webkit-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-ms-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-o-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.uil-ring-css {
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 200px;
height: 200px;
}
.uil-ring-css > div {
position: absolute;
display: block;
width: 160px;
height: 160px;
top: 20px;
left: 20px;
border-radius: 80px;
box-shadow: 0 6px 0 0 var(--primary-color);
-ms-animation: uil-ring-anim 1s linear infinite;
-moz-animation: uil-ring-anim 1s linear infinite;
-webkit-animation: uil-ring-anim 1s linear infinite;
-o-animation: uil-ring-anim 1s linear infinite;
animation: uil-ring-anim 1s linear infinite;
}
@@ -1,111 +0,0 @@
// Copy active HA theme
document.getElementsByTagName("html").item(0).setAttribute("style", parent.document.getElementsByTagName("html").item(0).style.cssText)
// Copy yhe content of the Lovelace example to the clipboard.
function CopyToLovelaceExampleToClipboard() {
window.getSelection().selectAllChildren(document.getElementById("LovelaceExample"));
document.execCommand("copy");
document.getSelection().empty()
document.getElementById('lovelacecopy').style.color = 'forestgreen';
}
// Show progress bar
function ShowProgressBar() {
document.getElementById('progressbar').style.display = 'block';
}
// Hard reloading
function HardReload() {
parent.location.reload(true)
}
// Searchbar
function Search() {
var input = document.getElementById("Search");
if (input) {
var filter = input.value.toLowerCase();
var nodes = document.getElementsByClassName('hacs-card');
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].innerHTML.toLowerCase().includes(filter)) {
nodes[i].style.display = "block";
} else {
nodes[i].style.display = "none";
}
}
var nodes = document.getElementsByClassName('hacs-table-row');
for (i = 0; i < nodes.length; i++) {
if (nodes[i].innerHTML.toLowerCase().includes(filter)) {
nodes[i].style.display = "table-row";
} else {
nodes[i].style.display = "none";
}
}
}
}
// Dropdown
document.addEventListener('DOMContentLoaded', function () {
var elems = document.querySelectorAll('.dropdown-trigger');
M.Dropdown.init(elems, { hover: false, constrainWidth: false });
});
// Modal
document.addEventListener('DOMContentLoaded', function () {
var elems = document.querySelectorAll('.modal');
M.Modal.init(elems, {});
});
// Loader
function toggleLoading() {
var loadingOverlay = document.querySelector('.loading');
loadingOverlay.classList.remove('hidden')
document.activeElement.blur();
}
// Check if we can reload
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
function CheckIfWeCanReload() {
var data = true;
const hacsrequest = new XMLHttpRequest()
hacsrequest.open('GET', '/hacs_task', true)
hacsrequest.onload = function () {
data = JSON.parse(this.response)
data = data["task"]
if (!data) {
console.log("Background task is no longer running, reloading in 5s...")
sleep(5000).then(() => {
location.reload()
});
}
}
hacsrequest.send()
}
function IsTaskRunning() {
let retval = false;
let disp = document.getElementsByClassName("progress")
if (disp) {
disp = disp.item(0).style.display;
if (disp == "block") {
retval = true
} else {
retval = false
}
}
return retval
}
window.setInterval(function () {
var running = false;
running = IsTaskRunning();
if (running) {
CheckIfWeCanReload();
}
}, 10000);
@@ -1,51 +0,0 @@
{% set progressbar = 'block' if hacs.system.status.background_task else 'none' %}
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/materialize.min.css.gz">
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/all.min.css.gz">
<script src="/hacsweb/{{ hacs.token }}/static/materialize.min.js.gz"></script>
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/static/hacs.css">
<script src="/hacsweb/{{ hacs.token }}/static/hacs.js"></script>
</head>
<body>
<div class="navbar-fixed">
<nav class="nav-extended hacs-nav">
<div class="nav-content">
<ul class="right tabs tabs-transparent">
<li class="tab {{ 'active' if location == 'overview' }}"><a href="/hacsweb/{{ hacs.token }}/overview?timestamp={{ timestamp }}">Overview</a></li>
<li class="tab {{ 'active' if location == 'store' }}"><a href="/hacsweb/{{ hacs.token }}/store?timestamp={{ timestamp }}">Store</a></li>
<li class="tab right {{ 'active' if location == 'settings' }}"><a href="/hacsweb/{{ hacs.token }}/settings?timestamp={{ timestamp }}">Settings</a></li>
</ul>
</div>
</nav>
</div>
<div id="main" class="hacs-content">
<div class="progressbar" style="display: {{ progressbar }}">
<p>Background task running, this page will reload when it's done.</p>
<div class="progress hacs-bar-background" id="progressbar">
<div class="indeterminate hacs-bar"></div>
</div>
</div>
<div class="loading hidden">
<div class='uil-ring-css' style='transform:scale(0.79);'>
<div></div>
</div>
</div>
{% if message %}
{% include 'message.html' with context %}
{% endif %}
{% set hacs_repo = hacs.get_by_name("hacs/integration") %}
{% if hacs_repo.pending_restart %}
{% set message = "The frontend of HACS has been disabled until you restart Home Assistant." %}
{% include 'message.html' with context %}
{% else %}
{% block content %}
{% endblock %}
{% endif %}
</div>
</body>
</html>
@@ -1,4 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% endblock %}
@@ -1,36 +0,0 @@
<div class='hacs-overview-container'>
<h5>{{ displaytype }}</h5>
<div class='hacs-card-container'>
{% for repository in repositories.repositories %}
<a href="/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}"
class="hacs-card" onclick="toggleLoading()">
<div class="hacs-card overview">
<div id="statusicon">
</div>
<div id="title">
<input type="hidden" name="author" value="{{repository.information.authors}}">
<input type="hidden" name="topics" value="{{repository.information.topics}}">
<input type="hidden" name="full_name" value="{{repository.information.full_name}}">
<span class="hacs-card-title">
{% if location == 'store' and repository.status.new %}
<span title="{{ repository.display_status_description }}" class='hacs-badge'>NEW</span>
{% else %}
<i title="{{ repository.display_status_description }}"
class='fas fa-cube card-status {{repository.display_status}}'></i>
{% endif %}
{{repository.display_name}}
</span>
</div>
<div id="description">
<span class="hacs-card-content">
<p>{{repository.information.description}}</p>
</span>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
</br>
</br>
@@ -1,47 +0,0 @@
{% set repositories = namespace(repositories=[]) %}
{% for repository in hacs.sorted_by_name %}
{% if repository.status.new %}
{% set repositories.repositories = repositories.repositories+[repository] %}
{% endif %}
{% endfor %}
{% if repositories.repositories %}
<div class='hacs-overview-container'>
<h5>NEW REPOSITORIES (ALL)</h5>
<form action="/hacsapi/{{ hacs.token }}/remove_new_flag" name="remove_new_flag" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="return" value="store">
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
onclick="toggleLoading();document.getElementsByName('remove_new_flag')[0].submit()" style="width: 95%;">
Remove the NEW flag for all new items
</a>
</form>
<div class='hacs-card-container'>
{% for repository in repositories.repositories %}
<a href="/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}"
class="hacs-card" onclick="toggleLoading()">
<div class="hacs-card overview">
<div id="statusicon">
</div>
<div id="title">
<span class="hacs-card-title">
<span class="hacs-badge">
{{repository.information.category | upper}}
</span>
</br></br>
{{repository.display_name}}
</span>
</div>
<div id="description">
<span class="hacs-card-content">
<p>{{repository.information.description}}</p>
</span>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
</br>
</br>
{% endif %}
@@ -1,13 +0,0 @@
<div class='container'>
<div class="row">
<div class="col s12">
<div class="card-panel orange darken-4">
<div class="card-content white-text">
<span>
{{message}}
</span>
</div>
</div>
</div>
</div>
</div>
@@ -1,33 +0,0 @@
<div id="modal_upgrade_all" class="modal hacscolor">
<div class="modal-content">
<h5>Pending Upgrades</h5>
{% for repository in hacs.repositories %}
{% if repository.pending_upgrade %}
<p>- {{repository.information.name}} (
{{repository.display_installed_version}}
->
{{repository.display_available_version}}
)</p>
{% endif %}
{% endfor %}
</br>
<p>
Be careful using this feature, elements may contain breaking changes,
make sure you read the release notes for all the elements in the list above.
</p>
</div>
<div class="modal-footer hacscolor">
{% if hacs.system.status.background_task %}
<p>Background task is running, batch upgrade is disabled.</p>
{% else %}
<form action="/hacsapi/{{ hacs.token }}/repositories_upgrade_all" name="upgrade_all" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
<a href="#" onclick="toggleLoading();document.getElementsByName('upgrade_all')[0].submit()"
class='waves-effect waves-light btn hacsbutton'
style="background-color: var(--google-red-500) !important; font-weight: bold;">
UPGRADE ALL
</a>
</form>
{% endif %}
</div>
</div>
@@ -1,12 +0,0 @@
<div id="haversion" class="modal hacscolor">
<div class="modal-content">
<h5>Unsupported Home Assistant version</h5>
<p>
You have version '{{hacs.system.ha_version}}' of Home Assistant, but version
'{{repository.display_available_version}}' of '{{repository.display_name}}' require version
'{{repository.repository_manifest.homeassistant}}' of Home Assistant, installation and upgrades are disabled
for this
integration untill you upgrade Home Assistant.
</p>
</div>
</div>
@@ -1,77 +0,0 @@
<!-- Used for both overview and store-->
{% extends "base.html" %}
{% block content %}
{% if location == 'store' %}
<div class='hacs-overview-container'>
<input type="text" id="Search" onkeyup="Search()" placeholder="Please enter a search term.." title="Type in a name"
autofocus style="color: var(--primary-text-color)">
</div>
{% if hacs.configuration.frontend_mode == "Grid" %}
{% include 'grid_new.html' with context %}
{% else %}
{% include 'table_new.html' with context %}
{% endif %}
{% endif %}
{% for type in hacs.common.categories %}
{% if type == 'appdaemon' %}
{% set displaytype = 'APPDAEMON APPS' %}
{% elif type == 'python_script' %}
{% set displaytype = 'PYTHON SCRIPTS' %}
{% else %}
{% set displaytype = (type + 's') | upper %}
{% endif %}
{% set repositories = namespace(repositories=[]) %}
{% for repository in hacs.sorted_by_name %}
{% set repo = namespace(include=True) %}
{% if repository.status.hide and repo.include %}
{% set repo.include = False %}
{% endif %}
{% if repository.information.category != type and repo.include %}
{% set repo.include = False %}
{% endif %}
{% if location == 'overview' %}
{% if not repository.status.installed and repo.include %}
{% set repo.include = False %}
{% if repository.information.uid == "172733314" %}
{% set repo.include = True %}
{% endif %}
{% endif %}
{% else %}
{% if repository.information.uid == "172733314" %}
{% set repo.include = False %}
{% endif %}
{% if hacs.configuration.country and repo.include %}
{% if repository.repository_manifest %}
{% if repository.repository_manifest.country %}
{% if hacs.configuration.country not in repository.repository_manifest.country %}
{% set repo.include = False %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% if repo.include %}
{% set repositories.repositories = repositories.repositories+[repository] %}
{% endif %}
{% endfor %}
{% if repositories.repositories %}
{% if hacs.configuration.frontend_mode == "Grid" %}
{% include 'grid.html' with context %}
{% else %}
{% include 'table.html' with context %}
{% endif %}
{% endif %}
{% endfor %}
{% endblock %}
@@ -1,93 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% if repository.pending_restart %}
{% set message = "You need to restart (and potentially reconfigure) Home Assistant, for your last operation to be loaded." %}
{% endif %}
{% if repository.information.category == "integration" and repository.status.first_install and repository.status.installed and repository.config_flow %}
{% set message = "
<b>Good news!</b></br>
This integration supports config flows!</br>
This means that you can go to 'Configuration' --> 'Integrations' --> '+' to configure it.</br>
The best part if you do that, is that you do <b>not</b> need to restart Home Assistant.</br></br>
<i>You may need to clear the cache on the integrations page before it shows in the list (+).</i>"%}
{% endif %}
{% if message %}
{% include 'message.html' with context %}
{% endif %}
<div class='hacs-overview-container'>
<div class="row">
<div class="col s12">
<div class="card hacscolor">
<div class="card-content">
<span class="card-title">
<b>{{repository.display_name}}</b>
<a class='dropdown-trigger btn right' href='#' data-target='dropdown1'
style="background-color: var(--primary-color); padding-top: 8px; height: 48px">
<i class="fas fa-bars"></i>
</a>
{% include 'repository/menu.html' with context %}
</span>
<p>{{repository.information.description}}</p></br>
<table class="hacs-table">
{% if repository.status.installed %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Installed {{repository.display_version_or_commit}}:</b>
</td>
<td class="repository text">
{{repository.display_installed_version}}
</td>
{% endif %}
</tr>
<tr class="hacs-table-row repository">
{% if repository.releases.published_tags %}
{% include 'repository/versionselect.html' with context %}
{% else %}
<td class="repository">
<b>Available {{repository.display_version_or_commit}}:</b>
</td>
<td class="repository text">
{{repository.display_available_version}}
</td>
{% endif %}
</tr>
{% if repository.status.selected_tag == repository.information.default_branch %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Last updated:</b>
</td>
<td class="repository text">
{{repository.information.last_updated}}
</td>
</tr>
{% endif %}
{% include 'repository/authors.html' with context %}
</table>
</div>
{% include 'repository/buttons.html' with context %}
</div>
</div>
</div>
</div>
<div class='hacs-overview-container'>
<div class="row">
<div class="col s12">
<div class="card hacscolor">
<div class="card-content" style="margin-top: -20px;">
{{ repository.information.additional_info }}
{% include 'repository/note.html' with context %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -1,27 +0,0 @@
{% if repository.information.authors %}
{% if repository.information.category == "integration" %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Author(s):</b>
</td>
<td class="repository text">
{% for author in repository.information.authors %}
{% if "@" in author %}
{% set author = author.split("@")[-1] %}
{% endif %}
<a rel='noreferrer' href='https://github.com/{{author}}' target='_blank'
style='color: var(--primary-color) !important; margin: 2'> @{{author}}</a>
{% endfor %}
</td>
</tr>
{% else %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Author:</b>
</td>
<td class="repository text">
{{repository.information.authors}}
</td>
</tr>
{% endif %}
{% endif %}
@@ -1,50 +0,0 @@
{% if not repository.can_install %}
{% include 'modal/wrong_ha_version.html' with context %}
{% endif %}
<div class="card-action">
{% if repository.can_install %}
<form action="/hacsapi/{{ hacs.token }}/repository_install" name="install" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a class="actionlink" href="#" onclick="toggleLoading();document.getElementsByName('install')[0].submit()">
{{repository.main_action}}
</a>
</form>
{% else %}
<a class="modal-trigger actionlink" href="#haversion">
{{repository.main_action}}
</a>
{% endif %}
{% if repository.pending_upgrade and repository.display_version_or_commit == "version" %}
<a class="actionlink" rel='noreferrer'
href='https://github.com/{{repository.information.full_name}}/releases/{{repository.available_version}}'
target='_blank'>
CHANGELOG
</a>
{% endif %}
<a class="actionlink" rel='noreferrer' href='https://github.com/{{repository.information.full_name}}' target='_blank'>
repository
</a>
{% if repository.status.installed and repository.information.category == "plugin" %}
{% set name = repository.information.full_name.split("/")[-1] %}
<a class="actionlink" href='/community_plugin/{{name}}/{{repository.information.file_name}}' target='_blank'>
OPEN PLUGIN
</a>
{% endif %}
{% if repository.status.installed and repository.information.uid != "172733314" %}
<form action="/hacsapi/{{ hacs.token }}/repository_uninstall" name="uninstall" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a class="actionlink uninstall" href='#'
onclick="toggleLoading();document.getElementsByName('uninstall')[0].submit()">
UNINSTALL
</a>
</form>
{% endif %}
</div>
@@ -1,61 +0,0 @@
<ul id='dropdown1' class='dropdown-content'>
<form action="/hacsapi/{{ hacs.token }}/repository_update" name="update" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<li>
<a class="dropdown-list-item" href='#' onclick="toggleLoading();document.getElementsByName('update')[0].submit()">
Reload
</a>
</li>
</form>
{% if repository.display_version_or_commit == "version" %}
{% set hide_show = "hide" if repository.status.show_beta else "show" %}
<form action="/hacsapi/{{ hacs.token }}/repository_beta_{{hide_show}}" name="beta" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<li>
<a class="dropdown-list-item" href='#' onclick="toggleLoading();document.getElementsByName('beta')[0].submit()">
{{hide_show | title}} beta
</a>
</li>
</form>
{% endif %}
{% if not repository.status.installed %}
{% if not repository.custom %}
{% if repository.information.uid != "172733314" %}
<form action="/hacsapi/{{ hacs.token }}/repository_hide" name="repository_hide" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<li>
<a class="dropdown-list-item" href='#'
onclick="toggleLoading();document.getElementsByName('repository_hide')[0].submit()">
Hide
</a>
</li>
</form>
{% endif %}
{% endif %}
{% endif %}
<li>
<a class="dropdown-list-item" rel='noreferrer'
href="https://github.com/{{repository.information.full_name}}/issues/" target="_blank">
Open issue
</a>
</li>
{% if repository.information.uid != "172733314" %}
<li>
<a class="dropdown-list-item" rel='noreferrer'
href="https://github.com/hacs/integration/issues/new?title={{repository.information.full_name}}&labels=flag&assignee=ludeeus&template=flag.md"
target="_blank">
Flag this
</a>
</li>
{% endif %}
</ul>
@@ -1,42 +0,0 @@
{% if repository.additional_info %}
</br>
<hr></br>
{% endif %}
{% if repository.information.category == "appdaemon" %}
<i>
When installed, this will be located in '{{repository.content.path.local}}',
you still need to add it to your 'apps.yaml' file.
</i>
{% elif repository.information.category == "integration" %}
<i>
When installed, this will be located in '{{repository.content.path.local}}',
you still need to add it to your 'configuration.yaml' file.
</i>
{% elif repository.information.category == "plugin" %}
{% set name = repository.information.full_name.split("/")[-1] %}
<i>
When installed, this will be located in '{{repository.content.path.local }}',
you still need to add it to your lovelace configuration ('ui-lovelace.yaml' or the raw UI config editor).
</i>
</br></br>
<i>
When you add this to your configuration use this:
</i></br>
<pre id="LovelaceExample" class="yaml">
- url: /community_plugin/{{name}}/{{repository.information.file_name}}
{%- if repository.information.javascript_type %}
type: {{repository.information.javascript_type}}
{%- else %}
type: Could not determine the type of this plugin, check the repository.
{%- endif -%}
</pre>
<a title="Copy content to clipboard" id="lovelacecopy" onclick="CopyToLovelaceExampleToClipboard()">
<i class="fa fa-copy"></i>
</a>
{% endif %}
@@ -1,24 +0,0 @@
{% set tags = repository.releases.published_tags | sort(True) %}
{% set tags = tags + [repository.information.default_branch] %}
{% if repository.status.selected_tag %}
{% set selected_tag = repository.status.selected_tag %}
{% else %}
{% set selected_tag = repository.versions.available %}
{% endif %}
<td class="repository">
<b>Available versions:</b>
</td>
<td class="repository">
<form action="/hacsapi/{{ hacs.token }}/repository_select_tag" name="selected_tag" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<select name="selected_tag" class="hacs-select repository"
onchange="toggleLoading();document.getElementsByName('selected_tag')[0].submit()">
{% for tag in tags %}
<option {{'selected' if selected_tag == tag else ''}} class='hacscolor' value='{{tag}}'>{{tag}}</option>
{% endfor %}
</select>
</form>
</td>
@@ -1,20 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% if hacs.configuration.dev %}
{% set message = "You have 'dev' enabled for HACS, this is not intended for regular use, no support will be given if you break something." %}
{% include 'message.html' with context %}
{% endif %}
{% include 'modal/upgrade_all.html' with context %}
{% include 'settings/hacs_info.html' with context %}
{% include 'settings/buttons.html' with context %}
{% include 'settings/custom_repositories.html' with context %}
{% include 'settings/hidden_repositories.html' with context %}
{% if hacs.configuration.dev and not hacs.system.status.background_task %}
{% include 'settings/developer.html' with context %}
{% endif %}
{% endblock %}
@@ -1,26 +0,0 @@
<div class='hacs-overview-container minigrid'>
<form action="/hacsapi/{{ hacs.token }}/repositories_reload" name="reload" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
onclick="toggleLoading();document.getElementsByName('reload')[0].submit()">
RELOAD DATA
</a>
</form>
<a href="#modal_upgrade_all" class="waves-effect waves-light btn modal-trigger hacsbutton"
style="background-color: var(--google-red-500) !important; font-weight: bold;">
UPGRADE ALL
</a>
<a href='https://github.com/hacs/integration' rel='noreferrer' class='waves-effect waves-light btn hacsbutton'
target="_blank">
HACS REPO
</a>
<a href='https://github.com/hacs/integration/issues' rel='noreferrer'
class='waves-effect waves-light btn hacsbutton' target="_blank">
OPEN ISSUE
</a>
</div>
@@ -1,62 +0,0 @@
<div class='hacs-overview-container'>
<div class="row">
<ul class="collection with-header hacslist">
<li class="collection-header hacscolor hacslist">
<h5>CUSTOM REPOSITORIES</h5>
</li>
{% if hacs.system.status.background_task %}
<li class="collection-item hacscolor hacslist">
Loading custom repositories...
</li>
{% else %}
{% for repository in hacs.sorted_by_repository_name %}
{% if repository.custom %}
<li class="collection-item hacscolor hacslist">
<div>
<a href="/hacsweb/{{ hacs.token }}/repository/{{repository.information.uid}}">
<span class="hacs-badge">
{{repository.information.category | upper}}
</span>
{{repository.information.full_name }}
</a>
{% if repository.status.installed %}
<i title="Remove is not possible when installed."
class="secondary-content fas fa-trash-alt disabledaction"></i>
{% else %}
<form action="/hacsapi/{{ hacs.token }}/repository_remove" name="remove" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a class="secondary-content" href="#"
onclick="toggleLoading();document.getElementsByName('remove')[0].submit()"
style=" color: var(--primary-color)">
<i title="Remove." class="fas fa-trash-alt"></i>
</a>
</form>
{% endif %}
</div>
</li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
{% if not hacs.system.status.background_task %}
<form class="minigrid" action="/hacsapi/{{ hacs.token }}/repository_register" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded">
<input class="custom_repository" id="custom_url" type="text" name="custom_url"
placeholder=" ADD CUSTOM REPOSITORY" style="width: 70%; color: var(--primary-text-color)">
<select name="repository_type" class="hacs-select custom_repository">
<option disabled selected value>Category</option>
{% for element_type in hacs.common.categories | sort %}
<option class='hacscolor' value='{{element_type}}'>{{element_type | title}}</option>
{% endfor %}
</select>
<button class="btn waves-effect waves-light right" type="submit" name="add" onclick="toggleLoading()"
style="background-color: var(--primary-color); height: 44px;">
<i class="fas fa-save"></i>
</button>
</form>
{% endif %}
</div>
</div>
@@ -1,12 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/remove_new_flag" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
REMOVE FLAGS
</button>
</form>
</div>
</div>
{% endblock %}
@@ -1,11 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% for repository in hacs.store.frontend|sort(attribute='name') %}
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/devrepository" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository" value="{{repository.information.uid}}">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
{{ repository.name }}
</button>
</form>
{% endfor %}
{% endblock %}
@@ -1,13 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/set_ha_version_action" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input class="hacscolor" type="text" name="ha_version" value="{{hacs.store.ha_version}}">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
SET
</button>
</form>
</div>
</div>
{% endblock %}
@@ -1,25 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/dev_template" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input class="hacscolor" type="text" name="repository_id" value="{{ hacs.dev_template_id }}">
<textarea class="hacscolor" name="template" style="width:100%;height:250px;"></textarea></br>
<button name="set" class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
TEST
</button>
<button name="clear" class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
CLEAR
</button>
</form>
</div>
</div>
{% if hacs.dev_template %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
{{ hacs.dev_template }}
</div>
</div>
{% endif %}
{% endblock %}
@@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<label>{{hacs.token}}</label>
</div>
</div>
{% endblock %}
@@ -1,19 +0,0 @@
<div class="hacs-overview-container hacs-color minigrid" style="margin-top: 30px;">
<div class="hacs-card-standalone">
<h5 style="margin-left: 5px;">DEVELOPER TOOLS</h5>
{% for button in [
"remove_new_flag",
"repositories",
"set_ha_version",
"token",
"template_test"
] %}
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/devview" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="view" value="{{button}}">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
{{ button.replace("_"," ")}}
</button>
</form>
{% endfor %}
</div>
</div>
@@ -1,38 +0,0 @@
{% if hacs.get_by_id("172733314").pending_restart %}
{% set pending_restart = " <b>(RESTART PENDING!)</b>" %}
{% else %}
{% set pending_restart = "" %}
{% endif %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<h5>HACS (Home Assistant Community Store)</h5>
<table class="hacs-table">
<tr class="hacs-table-row repository">
<td class="repository">
<b>Version:</b>
</td>
<td class="repository text">
{{hacs.version}}{{pending_restart}}
</td>
</tr>
<tr class="hacs-table-row repository">
<td class="repository">
<b>Display:</b>
</td>
<td class="repository">
<form action="/hacsapi/{{ hacs.token }}/frontend_mode" name="overview_display" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
<select name="view_type" class="hacs-select repository"
onchange="toggleLoading();document.getElementsByName('overview_display')[0].submit()">
{% for displayoption in ["Grid", "Table"] %}
<option {{"selected" if hacs.configuration.frontend_mode == displayoption else ""}} class="hacscolor"
value="{{displayoption}}">{{displayoption}}</option>
{% endfor %}
</select>
</form>
</td>
</tr>
</table>
</div>
</div>
@@ -1,42 +0,0 @@
{% set repositories = namespace(hidden=False) %}
{% for repository in hacs.repositories_list_repo %}
{% if repository.hide %}
{% set repositories.hidden = True %}
{% endif %}
{% endfor %}
{% if repositories.hidden %}
<div class='hacs-overview-container'>
<div class="row">
<ul class="collection with-header hacslist">
<li class="collection-header hacscolor hacslist">
<h5>HIDDEN REPOSITORIES</h5>
</li>
{% if hacs.system.status.background_task %}
<li class="collection-item hacscolor hacslist">
Loading hidden repositories...
</li>
{% else %}
{% for repository in hacs.repositories_list_repo %}
{% if repository.hide %}
<li class="collection-item hacscolor hacslist">
<div>
<form action="/hacsapi/{{ hacs.token }}/repository_unhide" name="remove" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a href="#" onclick="toggleLoading();document.getElementsByName('remove')[0].submit()"
style=" color: var(--primary-color)">
<i title="Unhide" class="fas fa-plus-circle" style="padding-right: 8px"></i>
</a>
{{repository.repository_name}}
<span class="hacs-badge">{{repository.repository_type}}</span>
</form>
</div>
</li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
</div>
</div>
{% endif %}
@@ -1,42 +0,0 @@
<div class='hacs-overview-container'>
<div class="row">
<h5>{{ displaytype }}</h5>
<table class="hacs-table">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th class="smal-hide">Description</th>
{% if location == 'overview' %}
<th class="smal-hide">Installed</th>
<th class="smal-hide">Available</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for repository in repositories.repositories %}
<tr class="hacs-table-row"
onclick="toggleLoading();window.location='/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}';">
<input type="hidden" name="author" value="{{repository.information.authors}}">
<input type="hidden" name="topics" value="{{repository.information.topics}}">
<input type="hidden" name="full_name" value="{{repository.information.full_name}}">
<td title="{{ repository.display_status_description }}">
{% if location == 'store' and repository.status.new %}
<span style='margin-left: 25%' class='repository-list-badge hacs-badge'>NEW</span>
{% else %}
<i style='margin-left: 25%' class='fas fa-cube card-status {{repository.display_status}}'></i></td>
{% endif %}
<td>
{{repository.display_name}}
</td>
<td class="hacs-card-content smal-hide">{{repository.information.description}}</td>
{% if location == 'overview' %}
<td class="smal-hide">{{repository.display_installed_version}}</td>
<td class="smal-hide">{{repository.display_available_version}}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -1,44 +0,0 @@
{% set repositories = namespace(repositories=[]) %}
{% for repository in hacs.sorted_by_name %}
{% if repository.status.new %}
{% set repositories.repositories = repositories.repositories+[repository] %}
{% endif %}
{% endfor %}
{% if repositories.repositories %}
<div class='hacs-overview-container'>
<div class="row">
<h5>NEW REPOSITORIES (ALL)</h5>
<form action="/hacsapi/{{ hacs.token }}/remove_new_flag" name="remove_new_flag" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="return" value="store">
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
onclick="toggleLoading();document.getElementsByName('remove_new_flag')[0].submit()" style="width: 95%;">
Remove the NEW flag for all new items
</a>
</form>
<table class="hacs-table">
<thead>
<tr>
<th>Category</th>
<th>Name</th>
<th class="smal-hide">Description</th>
</tr>
</thead>
<tbody>
{% for repository in repositories.repositories %}
<tr class="hacs-table-row"
onclick="toggleLoading();window.location='/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}';">
<td>
<span class="hacs-badge">{{repository.information.category | upper}}</span>
</td>
<td>
{{repository.display_name}}
</td>
<td class="hacs-card-content smal-hide">{{repository.information.description}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
@@ -1,391 +0,0 @@
"""Initialize the HACS base."""
# pylint: disable=unused-argument, bad-continuation
import json
import uuid
from datetime import timedelta
from homeassistant.helpers.event import async_call_later, async_track_time_interval
from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit
from integrationhelper import Logger
from ..const import ELEMENT_TYPES
from ..store import async_load_from_store, async_save_to_store
class HacsStatus:
"""HacsStatus."""
startup = False
background_task = False
reloading_data = False
upgrading_all = False
class HacsCommon:
"""Common for HACS."""
categories = []
blacklist = []
default = []
installed = []
skip = []
class System:
"""System info."""
status = HacsStatus()
config_path = None
ha_version = None
disabled = False
class Developer:
"""Developer settings/tools."""
template_id = "Repository ID"
template_content = ""
template_raw = ""
@property
def devcontainer(self):
"""Is it a devcontainer?"""
import os
if "DEVCONTAINER" in os.environ:
return True
return False
class Hacs:
"""The base class of HACS, nested thoughout the project."""
token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}"
hacsweb = f"/hacsweb/{token}"
hacsapi = f"/hacsapi/{token}"
repositories = []
repo = None
data_repo = None
developer = Developer()
data = None
configuration = None
logger = Logger("hacs")
github = None
hass = None
version = None
system = System()
tasks = []
common = HacsCommon()
@staticmethod
def init(hass, github_token):
"""Return a initialized HACS object."""
return Hacs()
def get_by_id(self, repository_id):
"""Get repository by ID."""
try:
for repository in self.repositories:
if repository.information.uid == repository_id:
return repository
except Exception: # pylint: disable=broad-except
pass
return None
def get_by_name(self, repository_full_name):
"""Get repository by full_name."""
try:
for repository in self.repositories:
if repository.information.full_name == repository_full_name:
return repository
except Exception: # pylint: disable=broad-except
pass
return None
def is_known(self, repository_full_name):
"""Return a bool if the repository is known."""
for repository in self.repositories:
if repository.information.full_name == repository_full_name:
return True
return False
@property
def sorted_by_name(self):
"""Return a sorted(by name) list of repository objects."""
return sorted(self.repositories, key=lambda x: x.display_name)
@property
def sorted_by_repository_name(self):
"""Return a sorted(by repository_name) list of repository objects."""
return sorted(self.repositories, key=lambda x: x.information.full_name)
async def register_repository(self, full_name, category, check=True):
"""Register a repository."""
from ..repositories.repository import RERPOSITORY_CLASSES
if full_name in self.common.skip:
if full_name != "hacs/integration":
self.logger.debug(f"Skipping {full_name}")
return
if category not in RERPOSITORY_CLASSES:
self.logger.error(f"{category} is not a valid repository category.")
return False
repository = RERPOSITORY_CLASSES[category](full_name)
if check:
try:
await repository.registration()
if repository.validate.errors:
self.common.skip.append(repository.information.full_name)
if not self.system.status.startup:
self.logger.error(f"Validation for {full_name} failed.")
return repository.validate.errors
repository.logger.info("Registration complete")
except AIOGitHubException as exception:
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
self.common.skip.append(repository.information.full_name)
if not self.system.status.startup:
self.logger.error(
f"Validation for {full_name} failed with {exception}."
)
return exception
self.hass.bus.async_fire(
"hacs/repository",
{
"id": 1337,
"action": "registration",
"repository": repository.information.full_name,
"repository_id": repository.information.uid,
},
)
self.repositories.append(repository)
async def startup_tasks(self):
"""Tasks tha are started after startup."""
self.system.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
await self.handle_critical_repositories_startup()
await self.handle_critical_repositories()
await self.load_known_repositories()
await self.clear_out_blacklisted_repositories()
self.tasks.append(
async_track_time_interval(
self.hass, self.recuring_tasks_installed, timedelta(minutes=30)
)
)
self.tasks.append(
async_track_time_interval(
self.hass, self.recuring_tasks_all, timedelta(minutes=800)
)
)
self.system.status.startup = False
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
async def handle_critical_repositories_startup(self):
"""Handled critical repositories during startup."""
alert = False
critical = await async_load_from_store(self.hass, "critical")
if not critical:
return
for repo in critical:
if not repo["acknowledged"]:
alert = True
if alert:
self.logger.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create(
title="URGENT!", message="**Check the HACS panel!**"
)
async def handle_critical_repositories(self):
"""Handled critical repositories during runtime."""
# Get critical repositories
instored = []
critical = []
was_installed = False
try:
critical = await self.data_repo.get_contents("critical")
critical = json.loads(critical.content)
except AIOGitHubException:
pass
if not critical:
self.logger.debug("No critical repositories")
return
stored_critical = await async_load_from_store(self.hass, "critical")
for stored in stored_critical or []:
instored.append(stored["repository"])
stored_critical = []
for repository in critical:
self.common.blacklist.append(repository["repository"])
repo = self.get_by_name(repository["repository"])
stored = {
"repository": repository["repository"],
"reason": repository["reason"],
"link": repository["link"],
"acknowledged": True,
}
if repository["repository"] not in instored:
if repo is not None and repo.installed:
self.logger.critical(
f"Removing repository {repository['repository']}, it is marked as critical"
)
was_installed = True
stored["acknowledged"] = False
# Uninstall from HACS
repo.remove()
await repo.uninstall()
stored_critical.append(stored)
# Save to FS
await async_save_to_store(self.hass, "critical", stored_critical)
# Resart HASS
if was_installed:
self.logger.critical("Resarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))
async def recuring_tasks_installed(self, notarealarg=None):
"""Recuring tasks for installed repositories."""
self.logger.debug(
"Starting recuring background task for installed repositories"
)
self.system.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
for repository in self.repositories:
if repository.status.installed:
try:
await repository.update_repository()
repository.logger.debug("Information update done.")
except AIOGitHubException:
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
self.logger.debug(
"Recuring background task for installed repositories done"
)
return
await self.handle_critical_repositories()
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
self.logger.debug("Recuring background task for installed repositories done")
async def recuring_tasks_all(self, notarealarg=None):
"""Recuring tasks for all repositories."""
self.logger.debug("Starting recuring background task for all repositories")
self.system.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
for repository in self.repositories:
try:
await repository.update_repository()
repository.logger.debug("Information update done.")
except AIOGitHubException:
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
self.logger.debug("Recuring background task for all repositories done")
return
await self.load_known_repositories()
await self.clear_out_blacklisted_repositories()
self.system.status.background_task = False
await self.data.async_write()
self.hass.bus.async_fire("hacs/status", {})
self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
self.logger.debug("Recuring background task for all repositories done")
async def clear_out_blacklisted_repositories(self):
"""Clear out blaclisted repositories."""
need_to_save = False
for repository in self.common.blacklist:
if self.is_known(repository):
repository = self.get_by_name(repository)
if repository.status.installed:
self.logger.error(
f"You have {repository.information.full_name} installed with HACS, this repositroy has been blacklisted, please consider removing it."
)
else:
need_to_save = True
repository.remove()
if need_to_save:
await self.data.async_write()
async def get_repositories(self):
"""Return a list of repositories."""
repositories = {}
if self.configuration.dev:
if self.developer.devcontainer:
repositories = {
"appdaemon": ["ludeeus/ad-hacs"],
"integration": ["ludeeus/integration-hacs"],
"plugin": ["maykar/compact-custom-header"],
"python_script": ["ludeeus/ps-hacs"],
"theme": ["ludeeus/theme-hacs"],
}
else:
for category in self.common.categories:
remote = await self.data_repo.get_contents(category)
repositories[category] = json.loads(remote.content)
if category == "plugin":
org = await self.github.get_org_repos("custom-cards")
for repo in org:
repositories[category].append(repo.full_name)
if category == "integration":
org = await self.github.get_org_repos("custom-components")
for repo in org:
repositories[category].append(repo.full_name)
if category == "theme":
org = await self.github.get_org_repos(
"home-assistant-community-themes"
)
for repo in org:
repositories[category].append(repo.full_name)
for category in repositories:
for repo in repositories[category]:
if repo not in self.common.default:
self.common.default.append(repo)
return repositories
async def load_known_repositories(self):
"""Load known repositories."""
self.logger.info("Loading known repositories")
blacklist = await self.data_repo.get_contents("blacklist")
repositories = await self.get_repositories()
for item in json.loads(blacklist.content):
if item not in self.common.blacklist:
self.common.blacklist.append(item)
for category in repositories:
for repo in repositories[category]:
if repo in self.common.blacklist:
continue
if self.is_known(repo):
continue
try:
await self.register_repository(repo, category)
except (AIOGitHubException, AIOGitHubRatelimit):
pass
@@ -1,72 +0,0 @@
"""Backup."""
import os
import shutil
import tempfile
from time import sleep
from integrationhelper import Logger
BACKUP_PATH = tempfile.gettempdir() + "/hacs_backup/"
class Backup:
"""Backup."""
def __init__(self, local_path, backup_path=BACKUP_PATH):
"""initialize."""
self.logger = Logger("hacs.backup")
self.local_path = local_path
self.backup_path = backup_path
self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}"
def create(self):
"""Create a backup in /tmp"""
if not os.path.exists(self.local_path):
return
if os.path.exists(self.backup_path):
shutil.rmtree(self.backup_path)
while os.path.exists(self.backup_path):
sleep(0.1)
os.makedirs(self.backup_path, exist_ok=True)
try:
if os.path.isfile(self.local_path):
shutil.copyfile(self.local_path, self.backup_path_full)
os.remove(self.local_path)
else:
shutil.copytree(self.local_path, self.backup_path_full)
shutil.rmtree(self.local_path)
while os.path.exists(self.local_path):
sleep(0.1)
self.logger.debug(
f"Backup for {self.local_path}, created in {self.backup_path_full}"
)
except Exception: # pylint: disable=broad-except
pass
def restore(self):
"""Restore from backup."""
if not os.path.exists(self.backup_path_full):
return
if os.path.isfile(self.backup_path_full):
if os.path.exists(self.local_path):
os.remove(self.local_path)
shutil.copyfile(self.backup_path_full, self.local_path)
else:
if os.path.exists(self.local_path):
shutil.rmtree(self.local_path)
while os.path.exists(self.local_path):
sleep(0.1)
shutil.copytree(self.backup_path_full, self.local_path)
self.logger.debug(
f"Restored {self.local_path}, from backup {self.backup_path_full}"
)
def cleanup(self):
"""Cleanup backup files."""
if os.path.exists(self.backup_path):
shutil.rmtree(self.backup_path)
while os.path.exists(self.backup_path):
sleep(0.1)
self.logger.debug(f"Backup dir {self.backup_path} cleared")
@@ -1,55 +0,0 @@
"""HACS Configuration."""
import attr
from custom_components.hacs.hacsbase.exceptions import HacsUserScrewupException
@attr.s(auto_attribs=True)
class Configuration:
"""Configuration class."""
# Main configuration:
appdaemon_path: str = "appdaemon/apps/"
appdaemon: bool = False
config: dict = {}
config_entry: dict = {}
config_type: str = None
dev: bool = False
frontend_mode: str = "Grid"
options: dict = {}
plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/"
python_script: bool = False
sidepanel_icon: str = "mdi:alpha-c-box"
sidepanel_title: str = "Community"
theme_path: str = "themes/"
theme: bool = False
token: str = None
# Config options:
country: str = "ALL"
experimental: bool = False
release_limit: int = 5
@staticmethod
def from_dict(configuration: dict, options: dict):
"""Set attributes from dicts."""
if isinstance(options, bool) or isinstance(configuration.get("options"), bool):
raise HacsUserScrewupException("Configuration is not valid.")
if options is None:
options = {}
if not configuration:
raise HacsUserScrewupException("Configuration is not valid.")
config = Configuration()
config.config = configuration
config.options = options
for conf_type in [configuration, options]:
for key in conf_type:
setattr(config, key, conf_type[key])
return config
@@ -1,10 +0,0 @@
"""Constants for HACS"""
# pylint: disable=unused-import
STORAGE_VERSION = "6"
STORENAME = "hacs"
# Messages
NOT_SUPPORTED_HA_VERSION = "You have version '{}' of Home Assistant, but version '{}' of '{}' require version '{}' of Home Assistant, install and upgrades are disabled for this integration untill you upgrade Home Assistant."
NO_ELEMENTS = "No elements to show, open the store to install some awesome stuff."
@@ -1,215 +0,0 @@
"""Data handler for HACS."""
import os
import json
from integrationhelper import Logger
from . import Hacs
from .const import STORAGE_VERSION
from ..const import VERSION
from ..repositories.manifest import HacsManifest
from ..store import async_save_to_store, async_load_from_store
STORES = {
"old": "hacs",
"hacs": "hacs.hacs",
"installed": "hacs.installed",
"repositories": "hacs.repositories",
}
class HacsData(Hacs):
"""HacsData class."""
def __init__(self):
"""Initialize."""
self.logger = Logger("hacs.data")
def check_corrupted_files(self):
"""Return True if one (or more) of the files are corrupted."""
for store in STORES:
path = f"{self.system.config_path}/.storage/{STORES[store]}"
if os.path.exists(path):
if os.stat(path).st_size == 0:
# File is empty (corrupted)
return True
return False
def read(self, store):
"""Return data from a store."""
path = f"{self.system.config_path}/.storage/{STORES[store]}"
content = None
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as storefile:
content = storefile.read()
content = json.loads(content)
return content
async def async_write(self):
"""Write content to the store files."""
if self.system.status.background_task:
return
self.logger.debug("Saving data")
# Hacs
await async_save_to_store(
self.hass, "hacs", {"view": self.configuration.frontend_mode}
)
# Repositories
content = {}
for repository in self.repositories:
if repository.repository_manifest is not None:
repository_manifest = repository.repository_manifest.manifest
else:
repository_manifest = None
content[repository.information.uid] = {
"authors": repository.information.authors,
"topics": repository.information.topics,
"category": repository.information.category,
"description": repository.information.description,
"full_name": repository.information.full_name,
"hide": repository.status.hide,
"installed_commit": repository.versions.installed_commit,
"installed": repository.status.installed,
"last_commit": repository.versions.available_commit,
"last_release_tag": repository.versions.available,
"repository_manifest": repository_manifest,
"name": repository.information.name,
"new": repository.status.new,
"selected_tag": repository.status.selected_tag,
"show_beta": repository.status.show_beta,
"version_installed": repository.versions.installed,
}
await async_save_to_store(self.hass, "repositories", content)
self.hass.bus.async_fire("hacs/repository", {})
self.hass.bus.fire("hacs/config", {})
async def restore(self):
"""Restore saved data."""
hacs = {}
repositories = {}
try:
hacs = await async_load_from_store(self.hass, "hacs")
except KeyError:
await async_save_to_store(self.hass, "hacs", self.data.read("hacs")["data"])
hacs = await async_load_from_store(self.hass, "hacs")
try:
repositories = await async_load_from_store(self.hass, "repositories")
except KeyError:
await async_save_to_store(
self.hass, "repositories", self.data.read("repositories")["data"]
)
repositories = await async_load_from_store(self.hass, "repositories")
try:
if self.check_corrupted_files():
# Coruptted installation
self.logger.critical("Restore failed one or more files are corrupted!")
return False
if hacs is None and repositories is None:
# Assume new install
return True
self.logger.info("Restore started")
# Hacs
self.configuration.frontend_mode = hacs.get("view", "Grid")
# Repositories
repositories = repositories
for entry in repositories:
repo = repositories[entry]
if repo["full_name"] == "custom-components/hacs":
# Skip the old repo location
continue
if not self.is_known(repo["full_name"]):
await self.register_repository(
repo["full_name"], repo["category"], False
)
repository = self.get_by_name(repo["full_name"])
if repository is None:
self.logger.error(f"Did not find {repo['full_name']}")
continue
# Restore repository attributes
if repo.get("authors") is not None:
repository.information.authors = repo["authors"]
if repo.get("topics", []):
repository.information.topics = repo["topics"]
if repo.get("description") is not None:
repository.information.description = repo["description"]
if repo.get("name") is not None:
repository.information.name = repo["name"]
if repo.get("hide") is not None:
repository.status.hide = repo["hide"]
if repo.get("installed") is not None:
repository.status.installed = repo["installed"]
if repository.status.installed:
repository.status.first_install = False
if repo.get("selected_tag") is not None:
repository.status.selected_tag = repo["selected_tag"]
if repo.get("repository_manifest") is not None:
repository.repository_manifest = HacsManifest.from_dict(
repo["repository_manifest"]
)
if repo.get("show_beta") is not None:
repository.status.show_beta = repo["show_beta"]
if repo.get("last_commit") is not None:
repository.versions.available_commit = repo["last_commit"]
repository.information.uid = entry
if repo.get("last_release_tag") is not None:
repository.releases.last_release = repo["last_release_tag"]
repository.versions.available = repo["last_release_tag"]
if repo.get("new") is not None:
repository.status.new = repo["new"]
if repo["full_name"] == "hacs/integration":
repository.versions.installed = VERSION
repository.status.installed = True
if "b" in VERSION:
repository.status.show_beta = True
elif repo.get("version_installed") is not None:
repository.versions.installed = repo["version_installed"]
if repo.get("installed_commit") is not None:
repository.versions.installed_commit = repo["installed_commit"]
self.logger.info("Restore done")
except Exception as exception:
self.logger.critical(
f"[{exception}] Restore Failed! see https://github.com/hacs/integration/issues/639 for more details."
)
return False
return True
def save(logger, path, content):
"""Save file."""
from .backup import Backup
backup = Backup(path)
backup.create()
try:
content = {"data": content, "schema": STORAGE_VERSION}
with open(path, "w", encoding="utf-8") as storefile:
json.dump(content, storefile, indent=4)
except Exception as exception: # pylint: disable=broad-except
logger.warning(f"Saving {path} failed - {exception}")
backup.restore()
backup.cleanup()
@@ -1,45 +0,0 @@
"""Custom Exceptions."""
class HacsBaseException(Exception):
"""Super basic."""
class HacsUserScrewupException(HacsBaseException):
"""Raise this when the user does something they should not do."""
class HacsNotSoBasicException(HacsBaseException):
"""Not that basic."""
class HacsDataFileMissing(HacsBaseException):
"""Raise this storage datafile is missing."""
class HacsDataNotExpected(HacsBaseException):
"""Raise this when data returned from storage is not ok."""
class HacsRepositoryInfo(HacsBaseException):
"""Raise this when repository info is missing/wrong."""
class HacsRequirement(HacsBaseException):
"""Raise this when repository is missing a requirement."""
class HacsMissingManifest(HacsBaseException):
"""Raise this when manifest is missing."""
def __init__(self, message="The manifest file is missing in the repository."):
super().__init__(message)
self.message = message
class HacsBlacklistException(HacsBaseException):
"""Raise this when the repository is currently in the blacklist."""
def __init__(self, message="The repository is currently in the blacklist."):
super().__init__(message)
self.message = message
@@ -1,129 +0,0 @@
"""HACS Migration logic."""
# pylint: disable=broad-except,no-member
from shutil import copy2
from integrationhelper import Logger, Validate
from . import Hacs
from .data import save, STORES
from .const import STORAGE_VERSION
MIGRATIONS = {}
def register(cls):
"""Register steps."""
MIGRATIONS[cls.from_version] = cls
return cls
class ValidateData(Hacs):
"""Validate."""
def validate_local_data_file(self):
"""Validate content."""
validate = Validate()
old_data = self.data.read("old")
if old_data is None:
# new install.
return True
if old_data.get("hacs", {}).get("schema") is None:
return True
if old_data.get("hacs", {}).get("schema") == STORAGE_VERSION:
# Newest version, no need to do anything.
return
current = old_data.get("hacs", {}).get("schema")
for version in range(int(current), int(STORAGE_VERSION)):
if current in MIGRATIONS:
MIGRATIONS[current](old_data).migrate()
else:
validate.errors.append(f"Missing migration step for {version}")
if validate.errors:
for error in validate.errors:
self.logger.critical(error)
return validate.success
class Migration(Hacs):
"""Hacs migrations"""
def __init__(self, old_data=None):
"""initialize migration."""
self.old_data = old_data
self.logger = Logger("hacs.migration")
self.source = f"{self.system.config_path}/.storage/hacs"
self.cleanup()
self.backup()
def cleanup(self):
"""Remove files no longer in use."""
def backup(self):
"""Back up old file."""
destination = f"{self.source}.{self.from_version}"
self.logger.info(f"Backing up current file to '{destination}'")
copy2(self.source, destination)
@register
class FromVersion4(Migration):
"""Migrate from version 4"""
from_version = "4"
def migrate(self):
"""Start migration."""
self.logger.info(f"Starting migration from {self.from_version}")
hacs = self.old_data["hacs"]
repositories = self.old_data["repositories"]
installed = {}
for repository in repositories:
repository = repositories[repository]
repository["full_name"] = repository["repository_name"]
repository["category"] = repository["repository_type"]
if not repository["installed"]:
repository["show_beta"] = False
else:
if repository["version_installed"] is not None:
version_type = "version"
version_installed = repository["version_installed"]
version_available = repository["last_release_tag"]
else:
version_type = "commit"
version_installed = repository["installed_commit"]
version_available = repository["last_commit"]
if repository["full_name"] != "hacs/integration":
installed[repository["repository_name"]] = {
"version_type": str(version_type),
"version_installed": str(version_installed),
"version_available": str(version_available),
}
path = f"{self.system.config_path}/.storage/{STORES['hacs']}"
save(self.logger, path, hacs)
path = f"{self.system.config_path}/.storage/{STORES['repositories']}"
save(self.logger, path, repositories)
path = f"{self.system.config_path}/.storage/{STORES['installed']}"
save(self.logger, path, installed)
self.logger.info("Migration done")
@register
class FromVersion5(Migration):
"""Migrate from version 5"""
from_version = "5"
def migrate(self):
"""Start migration."""
self.logger.info("Migration done")
@@ -1,16 +0,0 @@
"""HACS Startup."""
from . import Hacs
class HacsStartup(Hacs):
"""Startup class."""
async def run_startup(self):
"""Run startup tasks for HACS."""
self.logger.critical("Startup!")
if self.configuration.dev:
self.logger.critical("Running in DEV mode!")
if self.developer.devcontainer:
self.logger.critical("Running inside a devcontainer")
self.logger.critical("Some features have been disabled")
self.logger.critical("It will only fetch one repository pr category")
@@ -1,6 +0,0 @@
"""HACS Updater."""
from . import Hacs
class HacsUpdate(Hacs):
"""Update class."""

Some files were not shown because too many files have changed in this diff Show More