some new automations
This commit is contained in:
@@ -15,6 +15,10 @@ appdaemon:
|
||||
|
||||
api:
|
||||
|
||||
namespaces:
|
||||
worktime_namespace:
|
||||
writeback: safe
|
||||
|
||||
plugins:
|
||||
HASS:
|
||||
type: hass
|
||||
|
||||
@@ -19,8 +19,8 @@ class Intercom(hass.Hass):
|
||||
ringtone = 10
|
||||
gw_mac = ''
|
||||
def initialize(self):
|
||||
if "notify" not in self.args:
|
||||
self.error("Please provide notify in config!")
|
||||
if 'chat_id' not in self.args or "notify" not in self.args:
|
||||
self.error("Please provide chat_id, notify in config!")
|
||||
return
|
||||
if "gw_mac" in self.args:
|
||||
self.gw_mac = self.args['gw_mac']
|
||||
@@ -28,9 +28,30 @@ class Intercom(hass.Hass):
|
||||
if 'ringtone' in self.args:
|
||||
self.ringtone = self.args['ringtone']
|
||||
self.listen_event(self.domofon_call, "domofon_call")
|
||||
self.listen_event(self.receive_telegram_callback, 'telegram_callback')
|
||||
# self.domofon_call(None, None, None)
|
||||
|
||||
def domofon_call(self, event_id, event_args, kwargs):
|
||||
self.log("domofon_call")
|
||||
self.log(event_args)
|
||||
self.call_service("xiaomi_aqara/play_ringtone", gw_mac=self.gw_mac, ringtone_id=self.ringtone, ringtone_vol=100)
|
||||
self.notify("Звонок в домофон!!!", name = self.args['notify'])
|
||||
keyboard = [[("Открыть", "/domofon_open")]]
|
||||
self.call_service('telegram_bot/send_message',
|
||||
target=self.args['chat_id'],
|
||||
message="Звонок в домофон!!!",
|
||||
disable_notification=False,
|
||||
inline_keyboard=keyboard)
|
||||
|
||||
def receive_telegram_callback(self, event_id, payload_event, *args):
|
||||
"""Event listener for Telegram callback queries."""
|
||||
assert event_id == 'telegram_callback'
|
||||
data_callback = payload_event['data']
|
||||
callback_id = payload_event['id']
|
||||
chat_id = payload_event['chat_id']
|
||||
|
||||
if data_callback == '/domofon_open':
|
||||
self.turn_on("switch.domofon_open")
|
||||
self.turn_off("switch.domofon_open")
|
||||
self.call_service('telegram_bot/answer_callback_query',
|
||||
message='Готово',
|
||||
callback_query_id=callback_id)
|
||||
@@ -6,5 +6,6 @@ intercom:
|
||||
class: Intercom
|
||||
notify: telegram
|
||||
gw_mac: !secret aqara1_mac
|
||||
chat_id: !secret telegram_group_chat
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -21,15 +21,24 @@ class Watchdog(hass.Hass):
|
||||
listen_event_handle_list = []
|
||||
timezone = 3
|
||||
def initialize(self):
|
||||
if not globals.check_args(self, ['entity','update_interval', 'timeout']):
|
||||
if not globals.check_args(self, ['check_interval', 'timeout']):
|
||||
return
|
||||
self.timer = self.run_every(self.watchdog_check, self.datetime()+timedelta(seconds=10), self.args['update_interval'])
|
||||
self.timer = self.run_every(self.watchdog_check, self.datetime()+timedelta(seconds=10), self.args['check_interval'])
|
||||
|
||||
def watchdog_check(self, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
|
||||
entity = self.args['entity']
|
||||
entities = []
|
||||
if 'entity' in self.args:
|
||||
entities.append(self.args['entity'])
|
||||
if 'entities' in self.args:
|
||||
for entity in self.args['entities']:
|
||||
entities.append(entity)
|
||||
|
||||
# self.log(entities)
|
||||
for entity in entities:
|
||||
# entity = self.args['entity']
|
||||
attributes = self.get_state(entity, attribute='all')
|
||||
if not 'last_changed' in attributes:
|
||||
return
|
||||
@@ -42,10 +51,11 @@ class Watchdog(hass.Hass):
|
||||
friendly_name = self.friendly_name(entity)
|
||||
message = "Датчик {} ({}) не обновлялся в течении {} секунд.".format(friendly_name, entity, self.args['timeout'])
|
||||
self.notify(message, name = "telegram")
|
||||
|
||||
if 'reboot' in self.args and self.args['reboot']:
|
||||
self.reboot_sensor()
|
||||
|
||||
def reboot_sensor(self):
|
||||
if 'reboot' in self.args and self.args['reboot']:
|
||||
host = self.args['host']
|
||||
headers = {'Content-Type': 'application/json; charset=utf-8', 'Referer': 'http://{}/others.html'.format(host), 'X-Requested-With': 'XMLHttpRequest'}
|
||||
data = 'msg={"CID":20003,"PL":{}}'
|
||||
|
||||
@@ -5,10 +5,36 @@ watchdog_sensor_power_active_power:
|
||||
class: Watchdog
|
||||
module: watchdog
|
||||
entity: sensor.power_active_power
|
||||
update_interval: 60
|
||||
check_interval: 60
|
||||
timeout: 360
|
||||
reboot: True
|
||||
host: !secret modbus_host
|
||||
password: !secret modbus_admin_pass
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
watchdog_wemos_uptime:
|
||||
class: Watchdog
|
||||
module: watchdog
|
||||
entities:
|
||||
- sensor.uptime_airfresher
|
||||
- sensor.uptime_bath
|
||||
- sensor.uptime_domofon
|
||||
- sensor.uptime_kitchen_led
|
||||
- sensor.uptime_nest
|
||||
check_interval: 60
|
||||
timeout: 360
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
watchdog_sonoff_uptime:
|
||||
class: Watchdog
|
||||
module: watchdog
|
||||
entities:
|
||||
- sensor.uptime_bathlight
|
||||
- sensor.uptime_kitchen
|
||||
check_interval: 60
|
||||
timeout: 7200
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
import globals
|
||||
import datetime
|
||||
|
||||
#
|
||||
# Purifier controller
|
||||
# Turn on and off purifier by light sensor and timers
|
||||
#
|
||||
# Args:
|
||||
#
|
||||
# None
|
||||
#
|
||||
# Release Notes
|
||||
#
|
||||
# Version 1.0:
|
||||
# Initial Version
|
||||
|
||||
class Purifier(hass.Hass):
|
||||
away_fan_speed = 8 #1-14
|
||||
listen_event_handle_list = []
|
||||
mode = 'auto'
|
||||
|
||||
def initialize(self):
|
||||
if not globals.check_args(self, ["on_time", "off_time"]):
|
||||
return
|
||||
on_time = self.parse_time(self.args["on_time"])
|
||||
off_time = self.parse_time(self.args["off_time"])
|
||||
|
||||
self.on_timer = self.run_daily(self.day_mode_timer_event, on_time)
|
||||
self.off_timer = self.run_daily(self.night_mode_timer_event, off_time)
|
||||
|
||||
self.listen_event_handle_list.append(self.listen_event(self.away_mode_event, "away_mode"))
|
||||
self.listen_event_handle_list.append(self.listen_event(self.return_home_mode_event, "return_home_mode"))
|
||||
self.listen_event_handle_list.append(self.listen_state(self.state_change, self.args['entity'], attribute='illuminance'))
|
||||
|
||||
def state_change(self, entity, attribute, old, new, kwargs):
|
||||
self.log("{} changed from {} to {}".format(attribute, old, new))
|
||||
if old > 0 and new == 0 and self.time_is_between(self.datetime(), self.args["on_time"], self.args["off_time"]):
|
||||
self.night_mode()
|
||||
elif old == 0 and new > 0 and self.time_is_between(self.datetime(), self.args["on_time"], self.args["off_time"]):
|
||||
self.auto_mode()
|
||||
#self.auto_mode()
|
||||
|
||||
def return_home_mode_event(self, event_id, event_args, kwargs):
|
||||
self.auto_mode()
|
||||
|
||||
def away_mode_event(self, event_id, event_args, kwargs):
|
||||
self.away_mode()
|
||||
|
||||
def night_mode_timer_event(self, kwargs):
|
||||
self.night_mode()
|
||||
|
||||
def day_mode_timer_event(self, kwargs):
|
||||
self.auto_mode()
|
||||
|
||||
|
||||
def away_mode(self):
|
||||
self.mode='away'
|
||||
self.log('Set purifier mode to favorite, speed: {}'.format(self.away_fan_speed))
|
||||
if 'entity' in self.args:
|
||||
self.call_service("fan/set_speed", entity_id = self.args['entity'], speed='favorite')
|
||||
self.call_service("fan/xiaomi_miio_set_favorite_level", entity_id = self.args['entity'], level=self.away_fan_speed)
|
||||
self.call_service("fan/xiaomi_miio_set_led_on", entity_id = self.args['entity'])
|
||||
self.call_service("fan/xiaomi_miio_set_buzzer_on", entity_id = self.args['entity'])
|
||||
|
||||
def auto_mode(self):
|
||||
self.mode='auto'
|
||||
self.log('Set purifier mode to auto.')
|
||||
if 'entity' in self.args:
|
||||
self.call_service("fan/set_speed", entity_id = self.args['entity'], speed='auto')
|
||||
self.call_service("fan/xiaomi_miio_set_led_on", entity_id = self.args['entity'])
|
||||
self.call_service("fan/xiaomi_miio_set_buzzer_on", entity_id = self.args['entity'])
|
||||
|
||||
def night_mode(self):
|
||||
self.mode='night'
|
||||
self.log('Set purifier mode to silent.')
|
||||
if 'entity' in self.args:
|
||||
self.call_service("fan/xiaomi_miio_set_buzzer_off", entity_id = self.args['entity'])
|
||||
self.call_service("fan/set_speed", entity_id = self.args['entity'], speed='silent')
|
||||
self.call_service("fan/xiaomi_miio_set_led_off", entity_id = self.args['entity'])
|
||||
|
||||
|
||||
def time_is_between(
|
||||
hass, target_dt: datetime, start_time: str,
|
||||
end_time: str) -> bool:
|
||||
"""Generalization of AppDaemon's now_is_between method."""
|
||||
start_time_dt = hass.parse_time(start_time) # type: datetime
|
||||
end_time_dt = hass.parse_time(end_time) # type: datetime
|
||||
start_dt = target_dt.replace(
|
||||
hour=start_time_dt.hour,
|
||||
minute=start_time_dt.minute,
|
||||
second=start_time_dt.second)
|
||||
end_dt = target_dt.replace(
|
||||
hour=end_time_dt.hour,
|
||||
minute=end_time_dt.minute,
|
||||
second=end_time_dt.second)
|
||||
|
||||
if end_dt < start_dt:
|
||||
# Spans midnight
|
||||
if target_dt < start_dt and target_dt < end_dt:
|
||||
target_dt = target_dt + timedelta(days=1)
|
||||
end_dt = end_dt + timedelta(days=1)
|
||||
return start_dt <= target_dt <= end_dt
|
||||
@@ -0,0 +1,8 @@
|
||||
purifier_timer:
|
||||
module: purifier
|
||||
class: Purifier
|
||||
on_time: "06:45:00"
|
||||
off_time: "23:00:00"
|
||||
entity: fan.xiaomi_miio_device
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -0,0 +1,29 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
|
||||
#
|
||||
# Listen for homekit presence
|
||||
#
|
||||
# Args:
|
||||
# sensor - home presence 'sensor'
|
||||
# ha_panel - alarm control panel entity (to arm and disarm).
|
||||
# constraint - (optional, input_boolen), if turned off - alarm panel will be not armed\disarmed.
|
||||
#
|
||||
# Release Notes
|
||||
#
|
||||
# Version 1.0:
|
||||
# Initial Version
|
||||
|
||||
class HomekitPresense(hass.Hass):
|
||||
def initialize(self):
|
||||
if "sensor" not in self.args or "arrive_event" not in self.args or "leave_event" not in self.args:
|
||||
self.error("Please provide sensor, arrive_event, leave_event in config!")
|
||||
return
|
||||
self.listen_state(self.sensor_trigger, self.args['sensor'])
|
||||
|
||||
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||
if new == "on" and old == "off":
|
||||
self.fire_event(self.args['arrive_event'])
|
||||
elif new == "off" and old == "on":
|
||||
self.fire_event(self.args['leave_event'])
|
||||
self.log("{} turned {}".format(entity, new))
|
||||
self.notify("{} turned {}".format(entity, new), name = self.args['notify'])
|
||||
@@ -0,0 +1,35 @@
|
||||
#################################
|
||||
# HOMEKIT PRESENSE #
|
||||
#################################
|
||||
monster_home_presence:
|
||||
module: homekitpresense
|
||||
class: HomekitPresense
|
||||
notify: telegram_monster
|
||||
sensor: input_boolean.monster_home
|
||||
arrive_event: monster_home_arrive
|
||||
leave_event: monster_home_leave
|
||||
constraint: input_boolean.presence_control
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
monster_work_presence:
|
||||
module: homekitpresense
|
||||
class: HomekitPresense
|
||||
notify: telegram_monster
|
||||
sensor: input_boolean.monster_work
|
||||
arrive_event: monster_work_arrive
|
||||
leave_event: monster_work_leave
|
||||
constraint: input_boolean.presence_control
|
||||
global_dependencies:
|
||||
- globals
|
||||
|
||||
homekit_home_presence:
|
||||
module: homekitpresense
|
||||
class: HomekitPresense
|
||||
notify: telegram_monster
|
||||
sensor: input_boolean.homekit_presense
|
||||
arrive_event: homekit_home_arrive
|
||||
leave_event: homekit_home_leave
|
||||
constraint: input_boolean.presence_control
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -0,0 +1,37 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
import datetime
|
||||
#
|
||||
# Intercom auto open switch by presence
|
||||
#
|
||||
# Args:
|
||||
# entity_id - intercom autp open switch
|
||||
# arrive_event - Arrive event name.
|
||||
# leave_event - Leave event name
|
||||
# constraint - (optional, input_boolen), if turned off - no notifications will be sent.
|
||||
#
|
||||
# Release Notes
|
||||
#
|
||||
# Version 1.0:
|
||||
# Initial Version
|
||||
|
||||
class IntercomSwitch(hass.Hass):
|
||||
def initialize(self):
|
||||
if "entity_id" not in self.args or "arrive_event" not in self.args or "leave_event" not in self.args:
|
||||
self.error("Please provide entity_id, arrive_event, leave_event in config!")
|
||||
return
|
||||
self.listen_event(self.arrive_event, self.args['arrive_event'])
|
||||
self.listen_event(self.leave_event, self.args['leave_event'])
|
||||
|
||||
def arrive_event(self, event_id, event_args, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
entity_id = self.args['entity_id']
|
||||
self.log("Somebody come home, turn on {}".format(entity_id))
|
||||
self.turn_on(entity_id)
|
||||
|
||||
def leave_event(self, event_id, event_args, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
entity_id = self.args['entity_id']
|
||||
self.log("All persons leave home, turn off {}".format(entity_id))
|
||||
self.turn_off(entity_id)
|
||||
@@ -0,0 +1,11 @@
|
||||
#################################
|
||||
# WORK PRESENSE #
|
||||
#################################
|
||||
intercom_auto_open:
|
||||
module: intercom_switch
|
||||
class: IntercomSwitch
|
||||
entity_id: switch.domofon_mode
|
||||
arrive_event: homekit_home_arrive
|
||||
leave_event: homekit_home_leave
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -0,0 +1,116 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
import datetime
|
||||
#
|
||||
# Listen for homekit presence
|
||||
#
|
||||
# Args:
|
||||
# arrive_event - Arrive event name.
|
||||
# leave_event - Leave event name
|
||||
# notify - notify platform
|
||||
# constraint - (optional, input_boolen), if turned off - no notifications will be sent.
|
||||
#
|
||||
# Release Notes
|
||||
#
|
||||
# Version 1.0:
|
||||
# Initial Version
|
||||
|
||||
class WorkTime(hass.Hass):
|
||||
arrive_time = None
|
||||
leave_time = None
|
||||
notify_time = None
|
||||
app_namespace = "worktime_namespace"
|
||||
work_hours = 9
|
||||
notify_before_leave_in_minutes = 15
|
||||
notify_every_seconds = 300
|
||||
timers = []
|
||||
|
||||
def initialize(self):
|
||||
if 'notify' not in self.args or "arrive_event" not in self.args or "leave_event" not in self.args:
|
||||
self.error("Please provide arrive_event, leave_event in config!")
|
||||
return
|
||||
self.listen_event(self.arrive_event, self.args['arrive_event'])
|
||||
self.listen_event(self.leave_event, self.args['leave_event'])
|
||||
self.run_daily(self.reset_timer_tick, self.parse_time("23:59:59"))
|
||||
self.load_state()
|
||||
self.arrive_event(None, None, None)
|
||||
#self.save_state()
|
||||
|
||||
def arrive_event(self, event_id, event_args, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
if self.arrive_time != None:
|
||||
self.log('You have already arrived to the work today at {}'.format(self.arrive_time))
|
||||
return
|
||||
self.arrive_time = self.datetime()
|
||||
self.leave_time = self.datetime()+datetime.timedelta(hours=self.work_hours)
|
||||
self.notify_time = self.leave_time-datetime.timedelta(minutes=self.notify_before_leave_in_minutes)
|
||||
if self.notify_time<self.datetime():
|
||||
self.notify_time=self.datetime()+datetime.timedelta(seconds=self.notify_every_seconds)
|
||||
self.save_state()
|
||||
self.log("Monster has arrived to work at {}, work till {}, will notify at {}.".format(self.arrive_time, self.leave_time, self.notify_time))
|
||||
self.timers.append(self.run_every(self.timer_tick, self.notify_time, self.notify_every_seconds))
|
||||
self.notify("Добро пожаловать в офис. Сегодня работаешь до {}.".format(self.format_time(self.leave_time)), name = self.args['notify'])
|
||||
|
||||
def leave_event(self, event_id, event_args, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
if self.arrive_time == None:
|
||||
return
|
||||
self.cancel_timers()
|
||||
travelTime = ""
|
||||
if "work_to_home_time" in self.args:
|
||||
time_entity = self.get_state(self.args['work_to_home_time'], attribute='all')
|
||||
travel_time = time_entity['state']
|
||||
jams_rate = time_entity['attributes'].get('jamsrate', 0)
|
||||
travelTime = " Время в пути до дома: {} мин, пробки: {} баллов.".format(travel_time, jams_rate)
|
||||
|
||||
if self.datetime()<self.notify_time:
|
||||
self.log("Monster leave work too early: {}<{}".format(self.datetime(), self.notify_time))
|
||||
message = "Ты рано ушел с работы (работаешь с {} до {}).{}".format(self.format_time(self.arrive_time), self.format_time(self.leave_time), travelTime)
|
||||
else:
|
||||
self.log("Monster leave work at {} - {}".format(self.arrive_time, self.datetime))
|
||||
message = "Рабочий день окончен ({}-{}). Приятной дороги домой.{}".format(self.format_time(self.arrive_time), self.format_time(self.datetime()), travelTime)
|
||||
self.notify(message, name = self.args['notify'])
|
||||
|
||||
def timer_tick(self, args) -> None:
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
if self.datetime()<self.leave_time:
|
||||
return
|
||||
message = "Ты пришел на работу в {}, работаешь до {}. Пора домой!".format(self.format_time(self.arrive_time), self.format_time(self.leave_time))
|
||||
self.notify(message, name = self.args['notify'])
|
||||
|
||||
def cancel_timers(self) -> None:
|
||||
for timer in self.timers:
|
||||
self.cancel_timer(timer)
|
||||
self.timers = []
|
||||
|
||||
def reset_timer_tick(self, kwargs):
|
||||
self.arrive_time = None
|
||||
self.leave_time = None
|
||||
self.notify_time = None
|
||||
self.save_state()
|
||||
|
||||
def load_state(self):
|
||||
self.arrive_time=self.deserialize_datetime(self.get_state("sensor.arrive_time", namespace=self.app_namespace))
|
||||
self.leave_time=self.deserialize_datetime(self.get_state("sensor.leave_time", namespace=self.app_namespace))
|
||||
self.notify_time=self.deserialize_datetime(self.get_state("sensor.notify_time", namespace=self.app_namespace))
|
||||
self.log("App is reloaded. Saved values - arrive_time: {}, leave_time: {}, notify_time: {}".format(self.arrive_time, self.leave_time, self.notify_time))
|
||||
|
||||
def save_state(self):
|
||||
self.set_state("sensor.arrive_time", state=self.serialize_datetime(self.arrive_time), namespace=self.app_namespace)
|
||||
self.set_state("sensor.leave_time", state=self.serialize_datetime(self.leave_time), namespace=self.app_namespace)
|
||||
self.set_state("sensor.notify_time", state=self.serialize_datetime(self.notify_time), namespace=self.app_namespace)
|
||||
|
||||
def format_time(self, date):
|
||||
return date.strftime("%H:%M")
|
||||
|
||||
def serialize_datetime(self, date):
|
||||
if date is None:
|
||||
return None
|
||||
return date.isoformat()
|
||||
|
||||
def deserialize_datetime(seld, date):
|
||||
if (date is None):
|
||||
return None
|
||||
return datetime.datetime.fromisoformat(date)
|
||||
@@ -0,0 +1,13 @@
|
||||
#################################
|
||||
# WORK PRESENSE #
|
||||
#################################
|
||||
monster_worktime:
|
||||
module: worktime
|
||||
class: WorkTime
|
||||
notify: telegram_monster
|
||||
arrive_event: monster_work_arrive
|
||||
leave_event: monster_work_leave
|
||||
work_to_home_time: sensor.monster_travel_time_work_to_home
|
||||
constraint: input_boolean.work_presence_report
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -1,47 +0,0 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
|
||||
#
|
||||
# Work leave reminder
|
||||
# Send reminder about 'time to leave your work'
|
||||
#
|
||||
# Args:
|
||||
#
|
||||
# Release Notes
|
||||
#
|
||||
# Version 1.0:
|
||||
# Initial Version
|
||||
|
||||
class WorkPresence(hass.Hass):
|
||||
def initialize(self):
|
||||
self.listen_event(self.office_enter, "office_enter")
|
||||
self.listen_event(self.office_leave, "office_leave")
|
||||
self.listen_event(self.office_leave_reminder, "office_leave_reminder")
|
||||
|
||||
def office_leave_reminder(self, event_id, event_args, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
self.log('leave_reminder')
|
||||
enter_time = self.get_state('sensor.work_enter_time')
|
||||
leave_time = self.get_state('sensor.work_plan_leave_time')
|
||||
if 'payload' in event_args:
|
||||
leave_time = event_args['payload']
|
||||
message = "Ты пришел на работу в {}, работаешь до {}. Пора домой!".format(enter_time, leave_time)
|
||||
self.call_service('notify/telegram', message=message)
|
||||
|
||||
def office_leave(self, event_id, event_args, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
self.log('leave')
|
||||
work_to_home_time = self.get_state('sensor.work_to_home_time')
|
||||
message="Время в пути до дома {} минут.".format(work_to_home_time)
|
||||
self.call_service('notify/telegram', message=message)
|
||||
|
||||
def office_enter(self, event_id, event_args, kwargs):
|
||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||
return
|
||||
self.log('enter')
|
||||
enter_time = event_args.get('payload', None)
|
||||
if enter_time == '' or enter_time == None:
|
||||
return
|
||||
self.log(enter_time)
|
||||
self.call_service('notify/telegram', message='Добро пожаловать в офис. Приятного дня =)))')
|
||||
@@ -1,9 +0,0 @@
|
||||
#################################
|
||||
# WORK PRESENSE #
|
||||
#################################
|
||||
work_presence:
|
||||
module: work
|
||||
class: WorkPresence
|
||||
constraint: input_boolean.work_presence_report
|
||||
global_dependencies:
|
||||
- globals
|
||||
@@ -1,39 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
from notification import send_notification
|
||||
import datetime
|
||||
import sys, locale
|
||||
import logging
|
||||
#import unicode
|
||||
# from automation import Automation # type: ignore
|
||||
|
||||
"""
|
||||
Monitor events and output changes to the verbose_log. Nice for debugging purposes.
|
||||
Arguments:
|
||||
- events: List of events to monitor
|
||||
"""
|
||||
class Test(hass.Hass):
|
||||
def initialize(self) -> None:
|
||||
root_logger= logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG) # or whatever
|
||||
handler = logging.FileHandler('test.log', 'w', 'utf-8') # or whatever
|
||||
formatter = logging.Formatter('%(name)s %(message)s') # or whatever
|
||||
handler.setFormatter(formatter) # Pass handler as a parameter, not assign
|
||||
root_logger.addHandler(handler)
|
||||
UTF8Writer = codecs.getwriter('utf8')
|
||||
sys.stdout = UTF8Writer(sys.stdout.buffer)
|
||||
self.log(u"Привет")
|
||||
self.log(str(u'ô'))
|
||||
# id = send_notification(
|
||||
# self, targets="telegram_monster", message="Repeated message", title="title", when=datetime.datetime.now(), interval=10, iterations=2
|
||||
# )
|
||||
# self.log("id:{}".format(id))
|
||||
# id = send_notification(
|
||||
# self, targets="telegram_monster", message="Dummy", title="title"
|
||||
# )
|
||||
|
||||
def terminate(self):
|
||||
self.log('terminate')
|
||||
# for listen_event_handle in self.listen_event_handle_list:
|
||||
# self.cancel_listen_event(listen_event_handle)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import appdaemon.plugins.hass.hassapi as hass
|
||||
|
||||
class TelegramTest(hass.Hass):
|
||||
def initialize(self):
|
||||
"""Listen to Telegram Bot events of interest."""
|
||||
self.listen_event(self.receive_telegram_text, 'telegram_text')
|
||||
self.listen_event(self.receive_telegram_callback, 'telegram_callback')
|
||||
|
||||
def receive_telegram_text(self, event_id, payload_event, *args):
|
||||
"""Text repeater."""
|
||||
assert event_id == 'telegram_text'
|
||||
user_id = payload_event['user_id']
|
||||
msg = 'You said: ``` %s ```' % payload_event['text']
|
||||
keyboard = [[("Edit message", "/edit_msg"),
|
||||
("Don't", "/do_nothing")],
|
||||
[("Remove this button", "/remove button")]]
|
||||
self.call_service('telegram_bot/send_message',
|
||||
title='*Dumb automation*',
|
||||
target=user_id,
|
||||
message=msg,
|
||||
disable_notification=True,
|
||||
inline_keyboard=keyboard)
|
||||
|
||||
def receive_telegram_callback(self, event_id, payload_event, *args):
|
||||
"""Event listener for Telegram callback queries."""
|
||||
assert event_id == 'telegram_callback'
|
||||
data_callback = payload_event['data']
|
||||
callback_id = payload_event['id']
|
||||
chat_id = payload_event['chat_id']
|
||||
self.log(payload_event)
|
||||
# keyboard = ["Edit message:/edit_msg, Don't:/do_nothing",
|
||||
# "Remove this button:/remove button"]
|
||||
keyboard = [[("Edit message", "/edit_msg"),
|
||||
("Don't", "/do_nothing")],
|
||||
[("Remove this button", "/remove button")]]
|
||||
|
||||
if data_callback == '/edit_msg': # Message editor:
|
||||
# Answer callback query
|
||||
self.call_service('telegram_bot/answer_callback_query',
|
||||
message='Editing the message!',
|
||||
callback_query_id=callback_id,
|
||||
show_alert=True)
|
||||
|
||||
# Edit the message origin of the callback query
|
||||
msg_id = payload_event['message']['message_id']
|
||||
user = payload_event['from_first']
|
||||
title = '*Message edit*'
|
||||
msg = 'Callback received from %s. Message id: %s. Data: ``` %s ```'
|
||||
self.call_service('telegram_bot/edit_message',
|
||||
chat_id=chat_id,
|
||||
message_id=msg_id,
|
||||
title=title,
|
||||
message=msg % (user, msg_id, data_callback),
|
||||
inline_keyboard=keyboard)
|
||||
|
||||
elif data_callback == '/remove button': # Keyboard editor:
|
||||
# Answer callback query
|
||||
self.call_service('telegram_bot/answer_callback_query',
|
||||
message='Callback received for editing the '
|
||||
'inline keyboard!',
|
||||
callback_query_id=callback_id)
|
||||
|
||||
# Edit the keyboard
|
||||
new_keyboard = keyboard[:1]
|
||||
self.call_service('telegram_bot/edit_replymarkup',
|
||||
chat_id=chat_id,
|
||||
message_id='last',
|
||||
inline_keyboard=new_keyboard)
|
||||
|
||||
elif data_callback == '/do_nothing': # Only Answer to callback query
|
||||
self.call_service('telegram_bot/answer_callback_query',
|
||||
message='OK, you said no!',
|
||||
callback_query_id=callback_id)
|
||||
@@ -0,0 +1,3 @@
|
||||
telegram_test:
|
||||
module: telegram_test
|
||||
class: TelegramTest
|
||||
@@ -6,6 +6,7 @@ filter:
|
||||
- light
|
||||
- vacuum
|
||||
- cover
|
||||
- fan
|
||||
include_entities:
|
||||
- input_boolean.alice_night_mode
|
||||
- input_boolean.alice_turn_off_light
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
fan.xiaomi_miio_device:
|
||||
friendly_name: Воздухоочиститель
|
||||
@@ -6,6 +6,9 @@ filter:
|
||||
- light
|
||||
- vacuum
|
||||
- cover
|
||||
- fan
|
||||
- climate
|
||||
- camera
|
||||
include_entities:
|
||||
- sensor.humidity_158d0001149b3c
|
||||
- sensor.humidity_158d000117d37a
|
||||
@@ -15,6 +18,9 @@ filter:
|
||||
- sensor.temperature_158d000117d37a
|
||||
- sensor.temperature_158d00012d9ad7
|
||||
- sensor.temperature_158d000149c128
|
||||
- input_boolean.monster_work
|
||||
- input_boolean.monster_home
|
||||
- input_boolean.homekit_presense
|
||||
exclude_entities:
|
||||
- switch.plug_158d00010dd98d
|
||||
- switch.xiaomi_miio_switch
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
monster_work:
|
||||
name: Монстр на работе
|
||||
monster_home:
|
||||
name: Монстр дома
|
||||
homekit_presense:
|
||||
name: Присутсиве по HomeKit
|
||||
@@ -0,0 +1,21 @@
|
||||
- platform: mqtt
|
||||
state_topic: 'home/domofon/$uptime'
|
||||
name: 'uptime_domofon'
|
||||
- platform: mqtt
|
||||
state_topic: 'home/bath/$uptime'
|
||||
name: 'uptime_bath'
|
||||
- platform: mqtt
|
||||
state_topic: 'home/kitchen-led/$uptime'
|
||||
name: 'uptime_kitchen-led'
|
||||
- platform: mqtt
|
||||
state_topic: 'home/nest/$uptime'
|
||||
name: 'uptime_nest'
|
||||
- platform: mqtt
|
||||
state_topic: 'home/airfresher/$uptime'
|
||||
name: 'uptime_airfresher'
|
||||
- platform: mqtt
|
||||
state_topic: 'tele/bathlight/UPTIME'
|
||||
name: 'uptime_bathlight'
|
||||
- platform: mqtt
|
||||
state_topic: 'tele/kitchen/UPTIME'
|
||||
name: 'uptime_kitchen'
|
||||
@@ -0,0 +1,11 @@
|
||||
#################################################
|
||||
# CAR TRAVEL TIME #
|
||||
#################################################
|
||||
- platform: yandex_maps
|
||||
start: !secret home_coords
|
||||
destination: !secret work_coords
|
||||
name: monster_travel_time_home_to_work
|
||||
- platform: yandex_maps
|
||||
start: !secret work_coords
|
||||
destination: !secret home_coords
|
||||
name: monster_travel_time_work_to_home
|
||||
Binary file not shown.
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,421 @@
|
||||
"""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()}")
|
||||
@@ -0,0 +1,100 @@
|
||||
"""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)
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""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,
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
"""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",
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
"""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
|
||||
@@ -0,0 +1,49 @@
|
||||
"""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
|
||||
Binary file not shown.
@@ -0,0 +1,504 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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);
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
{% 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>
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
<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>
|
||||
@@ -0,0 +1,47 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
@@ -0,0 +1,33 @@
|
||||
<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>
|
||||
@@ -0,0 +1,12 @@
|
||||
<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>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- 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 %}
|
||||
@@ -0,0 +1,93 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,50 @@
|
||||
{% 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>
|
||||
@@ -0,0 +1,61 @@
|
||||
<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>
|
||||
@@ -0,0 +1,42 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% 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>
|
||||
@@ -0,0 +1,20 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<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>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{% 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 %}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{% 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 %}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{% 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 %}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="hacs-card-standalone">
|
||||
<label>{{hacs.token}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
<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>
|
||||
@@ -0,0 +1,38 @@
|
||||
{% 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>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,42 @@
|
||||
<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>
|
||||
@@ -0,0 +1,44 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,391 @@
|
||||
"""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
|
||||
@@ -0,0 +1,72 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""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
|
||||
@@ -0,0 +1,10 @@
|
||||
"""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."
|
||||
@@ -0,0 +1,215 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,45 @@
|
||||
"""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
|
||||
@@ -0,0 +1,129 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,16 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,6 @@
|
||||
"""HACS Updater."""
|
||||
from . import Hacs
|
||||
|
||||
|
||||
class HacsUpdate(Hacs):
|
||||
"""Update class."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Initialize handlers."""
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Download."""
|
||||
import os
|
||||
import gzip
|
||||
import shutil
|
||||
|
||||
import aiofiles
|
||||
import async_timeout
|
||||
from integrationhelper import Logger
|
||||
import backoff
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from ..hacsbase.exceptions import HacsNotSoBasicException
|
||||
|
||||
|
||||
@backoff.on_exception(backoff.expo, Exception, max_tries=5)
|
||||
async def async_download_file(hass, url):
|
||||
"""
|
||||
Download files, and return the content.
|
||||
"""
|
||||
logger = Logger("hacs.download.downloader")
|
||||
if url is None:
|
||||
return
|
||||
|
||||
# There is a bug somewhere... TODO: Find that bug....
|
||||
if "tags/" in url:
|
||||
url = url.replace("tags/", "")
|
||||
|
||||
logger.debug(f"Donwloading {url}")
|
||||
|
||||
result = None
|
||||
|
||||
with async_timeout.timeout(60, loop=hass.loop):
|
||||
request = await async_get_clientsession(hass).get(url)
|
||||
|
||||
# Make sure that we got a valid result
|
||||
if request.status == 200:
|
||||
result = await request.read()
|
||||
else:
|
||||
raise HacsNotSoBasicException(
|
||||
"Got status code {} when trying to download {}".format(
|
||||
request.status, url
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def async_save_file(location, content):
|
||||
"""Save files."""
|
||||
logger = Logger("hacs.download.save")
|
||||
logger.debug(f"Saving {location}")
|
||||
mode = "w"
|
||||
encoding = "utf-8"
|
||||
errors = "ignore"
|
||||
|
||||
if not isinstance(content, str):
|
||||
mode = "wb"
|
||||
encoding = None
|
||||
errors = None
|
||||
|
||||
try:
|
||||
async with aiofiles.open(
|
||||
location, mode=mode, encoding=encoding, errors=errors
|
||||
) as outfile:
|
||||
await outfile.write(content)
|
||||
outfile.close()
|
||||
|
||||
# Create gz for .js files
|
||||
if os.path.isfile(location):
|
||||
if location.endswith(".js") or location.endswith(".css"):
|
||||
with open(location, "rb") as f_in:
|
||||
with gzip.open(location + ".gz", "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
msg = "Could not write data to {} - {}".format(location, error)
|
||||
logger.debug(msg)
|
||||
|
||||
return os.path.exists(location)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Custom template support."""
|
||||
# pylint: disable=broad-except
|
||||
from jinja2 import Template
|
||||
from integrationhelper import Logger
|
||||
|
||||
|
||||
def render_template(content, context):
|
||||
"""Render templates in content."""
|
||||
# Fix None issues
|
||||
if context.releases.last_release_object is not None:
|
||||
prerelease = context.releases.last_release_object.prerelease
|
||||
else:
|
||||
prerelease = False
|
||||
|
||||
# Render the template
|
||||
try:
|
||||
render = Template(content)
|
||||
render = render.render(
|
||||
installed=context.status.installed,
|
||||
pending_update=context.pending_upgrade,
|
||||
prerelease=prerelease,
|
||||
selected_tag=context.status.selected_tag,
|
||||
version_available=context.releases.last_release,
|
||||
version_installed=context.display_installed_version,
|
||||
)
|
||||
return render
|
||||
except Exception as exception:
|
||||
Logger("hacs.template").debug(exception)
|
||||
return content
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Helper functions: misc"""
|
||||
import semantic_version
|
||||
|
||||
|
||||
def get_repository_name(
|
||||
hacs_manifest, repository_name: str, category: str = None, manifest: dict = None
|
||||
) -> str:
|
||||
"""Return the name of the repository for use in the frontend."""
|
||||
|
||||
if hacs_manifest.name is not None:
|
||||
return hacs_manifest.name
|
||||
|
||||
if category == "integration":
|
||||
if manifest:
|
||||
if "name" in manifest:
|
||||
return manifest["name"]
|
||||
|
||||
return repository_name.replace("-", " ").replace("_", " ").title()
|
||||
|
||||
|
||||
def version_is_newer_than_version(new: str, old: str) -> bool:
|
||||
"""Return a bool if source is newer than target, will also be true if identical."""
|
||||
if not isinstance(new, str) or not isinstance(old, str):
|
||||
return False
|
||||
if new == old:
|
||||
return True
|
||||
return semantic_version.Version.coerce(new) > semantic_version.Version.coerce(old)
|
||||
@@ -0,0 +1,262 @@
|
||||
"""Blueprint for HacsWebResponse."""
|
||||
import os
|
||||
from time import time
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from aiohttp import web
|
||||
|
||||
from integrationhelper import Logger
|
||||
|
||||
from .hacsbase import Hacs
|
||||
|
||||
|
||||
WEBRESPONSE = {}
|
||||
|
||||
|
||||
def webresponse(classname):
|
||||
"""Decorator used to register Web Responses."""
|
||||
WEBRESPONSE[classname.endpoint] = classname
|
||||
return classname
|
||||
|
||||
|
||||
class HacsExperimental(HomeAssistantView, Hacs):
|
||||
"""Base View Class for HACS."""
|
||||
|
||||
requires_auth = False
|
||||
name = "hacs"
|
||||
url = r"/hacs_experimental/{requested_file:.+}"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.logger = Logger("hacs.http")
|
||||
|
||||
async def get(self, request, requested_file): # pylint: disable=unused-argument
|
||||
"""Handle HACS Experimental Web requests."""
|
||||
servefile = f"{self.system.config_path}/custom_components/hacs/frontend/experimental/{requested_file}"
|
||||
|
||||
# Serve .gz file if it exist
|
||||
if os.path.exists(f"{servefile}.gz"):
|
||||
servefile += ".gz"
|
||||
|
||||
if os.path.exists(servefile):
|
||||
return web.FileResponse(servefile)
|
||||
else:
|
||||
return web.Response(status=404)
|
||||
|
||||
|
||||
class HacsWebResponse(HomeAssistantView, Hacs):
|
||||
"""Base View Class for HACS."""
|
||||
|
||||
requires_auth = False
|
||||
name = "hacs"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.logger = Logger("hacs.http")
|
||||
self.url = self.hacsweb + "/{path:.+}"
|
||||
self.endpoint = None
|
||||
self.postdata = None
|
||||
self.raw_headers = None
|
||||
self.repository_id = None
|
||||
self.request = None
|
||||
self.requested_file = None
|
||||
|
||||
async def get(self, request, path): # pylint: disable=unused-argument
|
||||
"""Handle HACS Web requests."""
|
||||
if self.system.disabled:
|
||||
return web.Response(status=404)
|
||||
self.endpoint = path.split("/")[0]
|
||||
self.raw_headers = request.raw_headers
|
||||
self.request = request
|
||||
self.requested_file = path.replace(self.endpoint + "/", "")
|
||||
self.repository_id = path.replace(self.endpoint + "/", "")
|
||||
if self.endpoint != "static":
|
||||
self.logger.debug(f"Endpoint ({self.endpoint}) called")
|
||||
if self.endpoint in WEBRESPONSE:
|
||||
try:
|
||||
response = WEBRESPONSE[self.endpoint]
|
||||
response = await response.response(self)
|
||||
except Exception as exception:
|
||||
render = self.render("error", message=exception)
|
||||
return web.Response(
|
||||
body=render, content_type="text/html", charset="utf-8"
|
||||
)
|
||||
else:
|
||||
# Return default response.
|
||||
response = await WEBRESPONSE["generic"].response(self)
|
||||
|
||||
# set headers
|
||||
response.headers["Cache-Control"] = "no-cache, must-revalidate, s-max_age=0"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
|
||||
# serve the response
|
||||
return response
|
||||
|
||||
def render(self, templatefile, location=None, repository=None, message=None):
|
||||
"""Render a template file."""
|
||||
loader = Environment(loader=PackageLoader("custom_components.hacs.frontend"))
|
||||
template = loader.get_template(templatefile + ".html")
|
||||
return template.render(
|
||||
{
|
||||
"hacs": self,
|
||||
"location": location,
|
||||
"repository": repository,
|
||||
"message": message,
|
||||
"timestamp": time(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class HacsPluginView(HacsWebResponse):
|
||||
"""Serve plugins."""
|
||||
|
||||
name = "hacs:plugin"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.url = r"/community_plugin/{requested_file:.+}"
|
||||
|
||||
async def get(self, request, requested_file): # pylint: disable=unused-argument
|
||||
"""Serve plugins for lovelace."""
|
||||
try:
|
||||
# Strip '?' from URL
|
||||
if "?" in requested_file:
|
||||
requested_file = requested_file.split("?")[0]
|
||||
|
||||
file = f"{self.system.config_path}/www/community/{requested_file}"
|
||||
|
||||
# Serve .gz if it exist
|
||||
if os.path.exists(file + ".gz"):
|
||||
file += ".gz"
|
||||
|
||||
response = None
|
||||
if os.path.exists(file):
|
||||
self.logger.debug("Serving {} from {}".format(requested_file, file))
|
||||
response = web.FileResponse(file)
|
||||
response.headers["Cache-Control"] = "max-age=0, must-revalidate"
|
||||
else:
|
||||
self.logger.error(f"Tried to serve up '{file}' but it does not exist")
|
||||
response = web.Response(status=404)
|
||||
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
self.logger.debug(
|
||||
"there was an issue trying to serve {} - {}".format(
|
||||
requested_file, error
|
||||
)
|
||||
)
|
||||
response = web.Response(status=404)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class HacsPlugin(HacsPluginView):
|
||||
"""Alias for HacsPluginView."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.url = r"/hacsplugin/{requested_file:.+}"
|
||||
|
||||
|
||||
@webresponse
|
||||
class Settings(HacsWebResponse):
|
||||
"""Serve HacsSettingsView."""
|
||||
|
||||
endpoint = "settings"
|
||||
|
||||
async def response(self):
|
||||
"""Serve HacsOverviewView."""
|
||||
message = self.request.rel_url.query.get("message")
|
||||
render = self.render("settings", "settings", message=message)
|
||||
return web.Response(body=render, content_type="text/html", charset="utf-8")
|
||||
|
||||
|
||||
@webresponse
|
||||
class Static(HacsWebResponse):
|
||||
"""Serve static files."""
|
||||
|
||||
endpoint = "static"
|
||||
|
||||
async def response(self):
|
||||
"""Serve static files."""
|
||||
servefile = f"{self.system.config_path}/custom_components/hacs/frontend/elements/{self.requested_file}"
|
||||
if os.path.exists(servefile + ".gz"):
|
||||
return web.FileResponse(servefile + ".gz")
|
||||
else:
|
||||
if os.path.exists(servefile):
|
||||
return web.FileResponse(servefile)
|
||||
else:
|
||||
return web.Response(status=404)
|
||||
|
||||
|
||||
@webresponse
|
||||
class Store(HacsWebResponse):
|
||||
"""Serve HacsOverviewView."""
|
||||
|
||||
endpoint = "store"
|
||||
|
||||
async def response(self):
|
||||
"""Serve HacsStoreView."""
|
||||
render = self.render("overviews", "store")
|
||||
return web.Response(body=render, content_type="text/html", charset="utf-8")
|
||||
|
||||
|
||||
@webresponse
|
||||
class Overview(HacsWebResponse):
|
||||
"""Serve HacsOverviewView."""
|
||||
|
||||
endpoint = "overview"
|
||||
|
||||
async def response(self):
|
||||
"""Serve HacsOverviewView."""
|
||||
render = self.render("overviews", "overview")
|
||||
return web.Response(body=render, content_type="text/html", charset="utf-8")
|
||||
|
||||
|
||||
@webresponse
|
||||
class Repository(HacsWebResponse):
|
||||
"""Serve HacsRepositoryView."""
|
||||
|
||||
endpoint = "repository"
|
||||
|
||||
async def response(self):
|
||||
"""Serve HacsRepositoryView."""
|
||||
message = self.request.rel_url.query.get("message")
|
||||
repository = self.get_by_id(str(self.repository_id))
|
||||
if repository is None:
|
||||
self.logger.error(f"No repository found with ID {str(self.repository_id)}")
|
||||
return web.Response(status=404)
|
||||
|
||||
if not repository.status.updated_info:
|
||||
await repository.update_repository()
|
||||
repository.status.updated_info = True
|
||||
|
||||
await self.data.async_write()
|
||||
|
||||
if repository.status.new:
|
||||
repository.status.new = False
|
||||
await self.data.async_write()
|
||||
|
||||
render = self.render("repository", repository=repository, message=message)
|
||||
return web.Response(body=render, content_type="text/html", charset="utf-8")
|
||||
|
||||
|
||||
@webresponse
|
||||
class Error(HacsWebResponse):
|
||||
"""Serve error page."""
|
||||
|
||||
endpoint = "error"
|
||||
|
||||
async def response(self):
|
||||
"""Serve error page."""
|
||||
# Generate content
|
||||
content = """
|
||||
<div class='center-align' style='margin-top: 20px'>
|
||||
<img rel="noreferrer" src='https://i.pinimg.com/originals/ec/85/67/ec856744fac64a5a9e407733f190da5a.png'>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return web.Response(
|
||||
body=self.render("error", message=content),
|
||||
content_type="text/html",
|
||||
charset="utf-8",
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"domain": "hacs",
|
||||
"name": "HACS (Home Assistant Community Store)",
|
||||
"documentation": "https://hacs.xyz",
|
||||
"dependencies": [
|
||||
"websocket_api",
|
||||
"frontend",
|
||||
"persistent_notification"
|
||||
],
|
||||
"config_flow": true,
|
||||
"codeowners": [
|
||||
"@ludeeus"
|
||||
],
|
||||
"homeassistant": "0.98.0",
|
||||
"requirements": [
|
||||
"aiofiles==0.4.0",
|
||||
"attrs==19.2.0",
|
||||
"aiogithubapi==0.4.2",
|
||||
"backoff==1.8.0",
|
||||
"integrationhelper==0.2.2",
|
||||
"semantic_version==2.8.2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Initialize repositories."""
|
||||
from .theme import HacsTheme
|
||||
from .integration import HacsIntegration
|
||||
from .python_script import HacsPythonScript
|
||||
from .appdaemon import HacsAppdaemon
|
||||
from .plugin import HacsPlugin
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Class for appdaemon apps in HACS."""
|
||||
from .repository import HacsRepository, register_repository_class
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsAppdaemon(HacsRepository):
|
||||
"""Appdaemon apps in HACS."""
|
||||
|
||||
category = "appdaemon"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.content.path.local = self.localpath
|
||||
self.content.path.remote = "apps"
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.system.config_path}/appdaemon/apps/{self.information.name}"
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
addir = await self.repository_object.get_contents("apps", self.ref)
|
||||
if not isinstance(addir, list):
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
self.content.path.remote = addir[0].path
|
||||
self.information.name = addir[0].name
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.system.status.startup:
|
||||
self.logger.error(error)
|
||||
return self.validate.success
|
||||
|
||||
async def registration(self):
|
||||
"""Registration."""
|
||||
if not await self.validate_repository():
|
||||
return False
|
||||
|
||||
# Run common registration steps.
|
||||
await self.common_registration()
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
async def update_repository(self):
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
return
|
||||
await self.common_update()
|
||||
|
||||
# Get appdaemon objects.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "apps":
|
||||
addir = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
self.content.path.remote = addir[0].path
|
||||
self.information.name = addir[0].name
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Class for integrations in HACS."""
|
||||
import json
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from homeassistant.loader import async_get_custom_components
|
||||
from .repository import HacsRepository, register_repository_class
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsIntegration(HacsRepository):
|
||||
"""Integrations in HACS."""
|
||||
|
||||
category = "integration"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.domain = None
|
||||
self.content.path.remote = "custom_components"
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.system.config_path}/custom_components/{self.domain}"
|
||||
|
||||
@property
|
||||
def config_flow(self):
|
||||
"""Return bool if integration has config_flow."""
|
||||
if self.manifest:
|
||||
if self.information.full_name == "hacs/integration":
|
||||
return False
|
||||
return self.manifest.get("config_flow", False)
|
||||
return False
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
await self.common_validate()
|
||||
|
||||
# Attach repository
|
||||
if self.repository_object is None:
|
||||
self.repository_object = await self.github.get_repo(
|
||||
self.information.full_name
|
||||
)
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "custom_components":
|
||||
ccdir = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
if not isinstance(ccdir, list):
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
for item in ccdir or []:
|
||||
if item.type == "dir":
|
||||
self.content.path.remote = item.path
|
||||
break
|
||||
|
||||
if self.repository_manifest.zip_release:
|
||||
self.content.objects = self.releases.last_release_object.assets
|
||||
|
||||
else:
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects or []:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
if not await self.get_manifest():
|
||||
self.validate.errors.append("Missing manifest file.")
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.system.status.startup:
|
||||
self.logger.error(error)
|
||||
return self.validate.success
|
||||
|
||||
async def registration(self):
|
||||
"""Registration."""
|
||||
if not await self.validate_repository():
|
||||
return False
|
||||
|
||||
# Run common registration steps.
|
||||
await self.common_registration()
|
||||
|
||||
# Get the content of the manifest file.
|
||||
await self.get_manifest()
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
async def update_repository(self):
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
return
|
||||
await self.common_update()
|
||||
|
||||
# Get integration objects.
|
||||
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "custom_components":
|
||||
ccdir = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
if not isinstance(ccdir, list):
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
self.content.path.remote = ccdir[0].path
|
||||
|
||||
try:
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
except AIOGitHubException:
|
||||
return
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects or []:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
await self.get_manifest()
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
async def reload_custom_components(self):
|
||||
"""Reload custom_components (and config flows)in HA."""
|
||||
self.logger.info("Reloading custom_component cache")
|
||||
del self.hass.data["custom_components"]
|
||||
await async_get_custom_components(self.hass)
|
||||
|
||||
async def get_manifest(self):
|
||||
"""Get info from the manifest file."""
|
||||
manifest_path = f"{self.content.path.remote}/manifest.json"
|
||||
try:
|
||||
manifest = await self.repository_object.get_contents(
|
||||
manifest_path, self.ref
|
||||
)
|
||||
manifest = json.loads(manifest.content)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return False
|
||||
|
||||
if manifest:
|
||||
self.manifest = manifest
|
||||
self.information.authors = manifest["codeowners"]
|
||||
self.domain = manifest["domain"]
|
||||
self.information.name = manifest["name"]
|
||||
self.information.homeassistant_version = manifest.get("homeassistant")
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
return True
|
||||
return False
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user