some new automations

This commit is contained in:
Your Name
2019-11-07 12:36:14 +03:00
parent 7a32cb74da
commit a00aaba52b
116 changed files with 6720 additions and 126 deletions
+4
View File
@@ -15,6 +15,10 @@ appdaemon:
api:
namespaces:
worktime_namespace:
writeback: safe
plugins:
HASS:
type: hass
+24 -3
View File
@@ -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
+32 -22
View File
@@ -21,34 +21,44 @@ 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']
attributes = self.get_state(entity, attribute='all')
if not 'last_changed' in attributes:
return
last_changed = attributes['last_changed']
last_changed_date = datetime.datetime.strptime(last_changed, '%Y-%m-%dT%H:%M:%S.%f+00:00') + timedelta(hours=self.timezone)
time_diff = datetime.datetime.now() - last_changed_date
if time_diff.seconds > self.args['timeout']:
self.log('Entity {} was changed {} seconds ago.'.format(entity, time_diff.seconds))
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)
friendly_name = self.friendly_name(entity)
message = "Датчик {} ({}) не обновлялся в течении {} секунд.".format(friendly_name, entity, self.args['timeout'])
self.notify(message, name = "telegram")
self.reboot_sensor()
# 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
last_changed = attributes['last_changed']
last_changed_date = datetime.datetime.strptime(last_changed, '%Y-%m-%dT%H:%M:%S.%f+00:00') + timedelta(hours=self.timezone)
time_diff = datetime.datetime.now() - last_changed_date
if time_diff.seconds > self.args['timeout']:
self.log('Entity {} was changed {} seconds ago.'.format(entity, time_diff.seconds))
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":{}}'
response = requests.post('http://{}/cmd'.format(host), headers=headers, data=data, auth=('admin', self.args['password']))
json = response.json()
self.log("reboot status: {}".format(json))
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":{}}'
response = requests.post('http://{}/cmd'.format(host), headers=headers, data=data, auth=('admin', self.args['password']))
json = response.json()
self.log("reboot status: {}".format(json))
@@ -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
-39
View File
@@ -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
+1
View File
@@ -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)
+421
View File
@@ -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
@@ -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);
@@ -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>
@@ -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>
@@ -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 %}
@@ -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 %}
@@ -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 %}
@@ -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>
@@ -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