This commit is contained in:
@@ -1,187 +0,0 @@
|
||||
"""
|
||||
Custom element manager for community created elements.
|
||||
|
||||
For more details about this integration, please refer to the documentation at
|
||||
https://hacs.xyz/
|
||||
"""
|
||||
import voluptuous as vol
|
||||
from aiogithubapi import AIOGitHub
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
|
||||
from .const import DEV_MODE, DOMAIN, ELEMENT_TYPES, STARTUP, VERSION
|
||||
from .constrains import check_constans
|
||||
from .hacsbase import Hacs
|
||||
from .hacsbase.configuration import Configuration
|
||||
from .hacsbase.data import HacsData
|
||||
from .hacsbase.migration import ValidateData
|
||||
from .setup import add_sensor, load_hacs_repository, setup_frontend
|
||||
|
||||
SCHEMA = hacs_base_config_schema()
|
||||
SCHEMA[vol.Optional("options")] = hacs_config_option_schema()
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: SCHEMA}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up this integration using yaml."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
hass.data[DOMAIN] = config
|
||||
Hacs.hass = hass
|
||||
Hacs.configuration = Configuration.from_dict(
|
||||
config[DOMAIN], config[DOMAIN].get("options")
|
||||
)
|
||||
Hacs.configuration.config_type = "yaml"
|
||||
await startup_wrapper_for_yaml(Hacs)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={}
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up this integration using UI."""
|
||||
conf = hass.data.get(DOMAIN)
|
||||
if config_entry.source == config_entries.SOURCE_IMPORT:
|
||||
if conf is None:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_remove(config_entry.entry_id)
|
||||
)
|
||||
return False
|
||||
Hacs.hass = hass
|
||||
Hacs.configuration = Configuration.from_dict(
|
||||
config_entry.data, config_entry.options
|
||||
)
|
||||
Hacs.configuration.config_type = "flow"
|
||||
Hacs.configuration.config_entry = config_entry
|
||||
config_entry.add_update_listener(reload_hacs)
|
||||
startup_result = await hacs_startup(Hacs)
|
||||
if not startup_result:
|
||||
raise ConfigEntryNotReady
|
||||
return startup_result
|
||||
|
||||
|
||||
async def startup_wrapper_for_yaml(hacs):
|
||||
"""Startup wrapper for yaml config."""
|
||||
startup_result = await hacs_startup(hacs)
|
||||
if not startup_result:
|
||||
hacs.hass.components.frontend.async_remove_panel(
|
||||
hacs.configuration.sidepanel_title.lower()
|
||||
.replace(" ", "_")
|
||||
.replace("-", "_")
|
||||
)
|
||||
hacs.logger.info("Could not setup HACS, trying again in 15 min")
|
||||
async_call_later(hacs.hass, 900, startup_wrapper_for_yaml(hacs))
|
||||
|
||||
|
||||
async def hacs_startup(hacs):
|
||||
"""HACS startup tasks."""
|
||||
hacs.logger.debug(f"Configuration type: {hacs.configuration.config_type}")
|
||||
hacs.version = VERSION
|
||||
hacs.logger.info(STARTUP)
|
||||
hacs.system.config_path = hacs.hass.config.path()
|
||||
hacs.system.ha_version = HAVERSION
|
||||
hacs.system.disabled = False
|
||||
hacs.github = AIOGitHub(
|
||||
hacs.configuration.token, async_create_clientsession(hacs.hass)
|
||||
)
|
||||
hacs.data = HacsData()
|
||||
|
||||
# Check HACS Constrains
|
||||
if not check_constans(hacs):
|
||||
if hacs.configuration.config_type == "flow":
|
||||
if hacs.configuration.config_entry is not None:
|
||||
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
|
||||
return False
|
||||
|
||||
# Set up frontend
|
||||
await setup_frontend(hacs)
|
||||
|
||||
# Load HACS
|
||||
if not await load_hacs_repository(hacs):
|
||||
if hacs.configuration.config_type == "flow":
|
||||
if hacs.configuration.config_entry is not None:
|
||||
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
|
||||
return False
|
||||
|
||||
val = ValidateData()
|
||||
if not val.validate_local_data_file():
|
||||
if hacs.configuration.config_type == "flow":
|
||||
if hacs.configuration.config_entry is not None:
|
||||
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
|
||||
return False
|
||||
|
||||
# Restore from storefiles
|
||||
if not await hacs.data.restore():
|
||||
hacs_repo = hacs().get_by_name("hacs/integration")
|
||||
hacs_repo.pending_restart = True
|
||||
if hacs.configuration.config_type == "flow":
|
||||
if hacs.configuration.config_entry is not None:
|
||||
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
|
||||
return False
|
||||
|
||||
# Add aditional categories
|
||||
if hacs.configuration.appdaemon:
|
||||
ELEMENT_TYPES.append("appdaemon")
|
||||
if hacs.configuration.python_script:
|
||||
ELEMENT_TYPES.append("python_script")
|
||||
if hacs.configuration.theme:
|
||||
ELEMENT_TYPES.append("theme")
|
||||
hacs.common.categories = sorted(ELEMENT_TYPES)
|
||||
|
||||
# Setup startup tasks
|
||||
if hacs.configuration.config_type == "yaml":
|
||||
hacs.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, hacs().startup_tasks()
|
||||
)
|
||||
else:
|
||||
async_call_later(hacs.hass, 5, hacs().startup_tasks())
|
||||
|
||||
# Print DEV warning
|
||||
if hacs.configuration.dev:
|
||||
hacs.logger.warning(DEV_MODE)
|
||||
hacs.hass.components.persistent_notification.create(
|
||||
title="HACS DEV MODE", message=DEV_MODE, notification_id="hacs_dev_mode"
|
||||
)
|
||||
|
||||
# Add sensor
|
||||
add_sensor(hacs)
|
||||
|
||||
# Set up services
|
||||
# await add_services(hacs)
|
||||
|
||||
# Mischief managed!
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass, config_entry):
|
||||
"""Handle removal of an entry."""
|
||||
Hacs().logger.info("Disabling HACS")
|
||||
Hacs().logger.info("Removing recuring tasks")
|
||||
for task in Hacs().tasks:
|
||||
task()
|
||||
Hacs().logger.info("Removing sensor")
|
||||
try:
|
||||
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
|
||||
except ValueError:
|
||||
pass
|
||||
Hacs().logger.info("Removing sidepanel")
|
||||
try:
|
||||
hass.components.frontend.async_remove_panel("hacs_web")
|
||||
except AttributeError:
|
||||
pass
|
||||
Hacs().system.disabled = True
|
||||
Hacs().logger.info("HACS is now disabled")
|
||||
|
||||
|
||||
async def reload_hacs(hass, config_entry):
|
||||
"""Reload HACS."""
|
||||
await async_remove_entry(hass, config_entry)
|
||||
await async_setup_entry(hass, config_entry)
|
||||
@@ -1,421 +0,0 @@
|
||||
"""API Endpoins."""
|
||||
from time import time
|
||||
from aiohttp import web
|
||||
|
||||
from integrationhelper import Logger
|
||||
|
||||
from .http import HacsWebResponse
|
||||
|
||||
APIRESPONSE = {}
|
||||
|
||||
|
||||
def apiresponse(classname):
|
||||
"""Decorator used to register API Responses."""
|
||||
APIRESPONSE[classname.name] = classname
|
||||
return classname
|
||||
|
||||
|
||||
class HacsAPI(HacsWebResponse):
|
||||
"""HacsAPI class."""
|
||||
|
||||
name = "hacsapi"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.logger = Logger("hacs.api")
|
||||
self.url = self.hacsapi + "/{endpoint}"
|
||||
|
||||
async def post(self, request, endpoint): # pylint: disable=unused-argument
|
||||
"""Handle HACS API requests."""
|
||||
if self.system.disabled:
|
||||
return web.Response(status=404)
|
||||
self.endpoint = endpoint
|
||||
self.postdata = await request.post()
|
||||
self.raw_headers = request.raw_headers
|
||||
self.request = request
|
||||
self.logger.debug(f"Endpoint ({endpoint}) called")
|
||||
if self.configuration.dev:
|
||||
self.logger.debug(f"Raw headers ({self.raw_headers})")
|
||||
self.logger.debug(f"Postdata ({self.postdata})")
|
||||
if self.endpoint in APIRESPONSE:
|
||||
try:
|
||||
response = APIRESPONSE[self.endpoint]
|
||||
response = await response.response(self)
|
||||
except Exception as exception:
|
||||
render = self.render(f"error", message=exception)
|
||||
return web.Response(
|
||||
body=render, content_type="text/html", charset="utf-8"
|
||||
)
|
||||
else:
|
||||
# Return default response.
|
||||
response = await APIRESPONSE["generic"].response(self)
|
||||
|
||||
# set headers
|
||||
response.headers["Cache-Control"] = "max-age=0, must-revalidate"
|
||||
|
||||
# serve the response
|
||||
return response
|
||||
|
||||
|
||||
class HacsRunningTask(HacsAPI):
|
||||
"""Return if BG task is running."""
|
||||
|
||||
name = "hacs:task"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.url = "/hacs_task"
|
||||
|
||||
async def get(self, request): # pylint: disable=unused-argument
|
||||
"""Handle GET request."""
|
||||
return web.json_response({"task": self.system.status.background_task})
|
||||
|
||||
|
||||
@apiresponse
|
||||
class Generic(HacsAPI):
|
||||
"""Generic API response."""
|
||||
|
||||
name = "generic"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
self.logger.error(f"Unknown endpoint '{self.endpoint}'")
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RemoveNewFlag(HacsAPI):
|
||||
"""Remove new flag on all repositories."""
|
||||
|
||||
name = "remove_new_flag"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
for repository in self.repositories:
|
||||
repository.status.new = False
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/{self.postdata['return']}?timestamp={time()}"
|
||||
)
|
||||
|
||||
|
||||
@apiresponse
|
||||
class DevTemplate(HacsAPI):
|
||||
"""Remove new flag on all repositories."""
|
||||
|
||||
name = "dev_template"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
from .handler.template import render_template
|
||||
|
||||
if "set" in self.postdata:
|
||||
self.developer.template_id = self.postdata.get("repository_id")
|
||||
repository = self.get_by_id(self.developer.template_id)
|
||||
template = render_template(self.postdata.get("template", ""), repository)
|
||||
info = await self.github.render_markdown(template)
|
||||
info = info.replace("<h3>", "<h6>").replace("</h3>", "</h6>")
|
||||
info = info.replace("<h2>", "<h5>").replace("</h2>", "</h5>")
|
||||
info = info.replace("<h1>", "<h4>").replace("</h1>", "</h4>")
|
||||
info = info.replace("<code>", "<code class='codeinfo'>")
|
||||
info = info.replace(
|
||||
'<a href="http', '<a rel="noreferrer" target="_blank" href="http'
|
||||
)
|
||||
info = info.replace("<ul>", "")
|
||||
info = info.replace("</ul>", "")
|
||||
self.developer.template_content = info
|
||||
else:
|
||||
self.developer.template_content = ""
|
||||
self.developer.template_id = "Repository ID"
|
||||
render = self.render("settings/dev/template_test")
|
||||
return web.Response(body=render, content_type="text/html", charset="utf-8")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class DevView(HacsAPI):
|
||||
"""Set HA version view."""
|
||||
|
||||
name = "devview"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
render = self.render(f"settings/dev/{self.postdata['view']}")
|
||||
return web.Response(body=render, content_type="text/html", charset="utf-8")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class SetHAVersionAction(HacsAPI):
|
||||
"""Set HA version action."""
|
||||
|
||||
name = "set_ha_version_action"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
self.common.ha_version = self.postdata["ha_version"]
|
||||
render = self.render("settings/dev/set_ha_version")
|
||||
return web.Response(body=render, content_type="text/html", charset="utf-8")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryInstall(HacsAPI):
|
||||
"""Install repository."""
|
||||
|
||||
name = "repository_install"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
await repository.install()
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
|
||||
)
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryUpdate(HacsAPI):
|
||||
"""Update repository."""
|
||||
|
||||
name = "repository_update"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
await repository.update_repository()
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
|
||||
)
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryUninstall(HacsAPI):
|
||||
"""Uninstall repository."""
|
||||
|
||||
name = "repository_uninstall"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
await repository.uninstall()
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/overview?timestamp={time()}")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryRemove(HacsAPI):
|
||||
"""Remove repository."""
|
||||
|
||||
name = "repository_remove"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
await repository.remove()
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryHide(HacsAPI):
|
||||
"""Hide repository."""
|
||||
|
||||
name = "repository_hide"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
repository.hide = True
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/store?timestamp={time()}")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryUnhide(HacsAPI):
|
||||
"""Unhide repository."""
|
||||
|
||||
name = "repository_unhide"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
repository.hide = False
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryBetaHide(HacsAPI):
|
||||
"""Hide Beta repository."""
|
||||
|
||||
name = "repository_beta_hide"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
repository.status.show_beta = False
|
||||
await repository.update_repository()
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
|
||||
)
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryBetaShow(HacsAPI):
|
||||
"""Show Beta repository."""
|
||||
|
||||
name = "repository_beta_show"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
repository.status.show_beta = True
|
||||
await repository.update_repository()
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
|
||||
)
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoriesReload(HacsAPI):
|
||||
"""Reload repository data."""
|
||||
|
||||
name = "repositories_reload"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
self.hass.async_create_task(self.recuring_tasks_all())
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoriesUpgradeAll(HacsAPI):
|
||||
"""Upgrade all repositories."""
|
||||
|
||||
name = "repositories_upgrade_all"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
for repository in self.repositories:
|
||||
if repository.pending_upgrade:
|
||||
await repository.install()
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositoryRegister(HacsAPI):
|
||||
"""Register repository."""
|
||||
|
||||
name = "repository_register"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
repository_name = self.postdata.get("custom_url")
|
||||
repository_type = self.postdata.get("repository_type")
|
||||
|
||||
# Validate data
|
||||
if not repository_name:
|
||||
message = "Repository URL is missing."
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
|
||||
)
|
||||
if repository_type is None:
|
||||
message = "Type is missing for '{}'.".format(repository_name)
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
|
||||
)
|
||||
|
||||
# Stip first part if it's an URL.
|
||||
if "github" in repository_name:
|
||||
repository_name = repository_name.split("github.com/")[-1]
|
||||
|
||||
# Strip whitespace
|
||||
repository_name = repository_name.split()[0]
|
||||
|
||||
# If it still have content, continue.
|
||||
if repository_name != "":
|
||||
if len(repository_name.split("/")) != 2:
|
||||
message = f"""
|
||||
{repository_name} is not a valid format.
|
||||
Correct format is 'https://github.com/DEVELOPER/REPOSITORY'
|
||||
or 'DEVELOPER/REPOSITORY'.
|
||||
"""
|
||||
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
|
||||
)
|
||||
|
||||
is_known_repository = self.is_known(repository_name)
|
||||
if is_known_repository:
|
||||
message = f"'{repository_name}' is already registered, look for it in the store."
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
|
||||
)
|
||||
|
||||
if repository_name in self.common.blacklist:
|
||||
self.common.blacklist.remove(repository_name)
|
||||
|
||||
await self.register_repository(repository_name, repository_type)
|
||||
|
||||
repository = self.get_by_name(repository_name)
|
||||
if repository is not None:
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
|
||||
)
|
||||
|
||||
message = f"""
|
||||
Could not add '{repository_name}' with type '{repository_type}' at this time.</br>
|
||||
If you used the correct type, check the log for more details."""
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
|
||||
)
|
||||
|
||||
|
||||
@apiresponse
|
||||
class RepositorySelectTag(HacsAPI):
|
||||
"""Select tag for Repository."""
|
||||
|
||||
name = "repository_select_tag"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from .hacsbase.exceptions import HacsRequirement
|
||||
|
||||
repository = self.get_by_id(self.postdata["repository_id"])
|
||||
if self.postdata["selected_tag"] == repository.releases.last_release:
|
||||
repository.status.selected_tag = None
|
||||
else:
|
||||
repository.status.selected_tag = self.postdata["selected_tag"]
|
||||
|
||||
try:
|
||||
await repository.update_repository()
|
||||
except (AIOGitHubException, HacsRequirement):
|
||||
repository.status.selected_tag = repository.releases.last_release
|
||||
await repository.update_repository()
|
||||
message = "The version {} is not valid for use with HACS.".format(
|
||||
self.postdata["selected_tag"]
|
||||
)
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}&message={message}"
|
||||
)
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(
|
||||
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
|
||||
)
|
||||
|
||||
|
||||
@apiresponse
|
||||
class FrontentMode(HacsAPI):
|
||||
"""Set the frontend mode."""
|
||||
|
||||
name = "frontend_mode"
|
||||
|
||||
async def response(self):
|
||||
"""Response."""
|
||||
self.configuration.frontend_mode = self.postdata["view_type"]
|
||||
await self.data.async_write()
|
||||
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
|
||||
@@ -1,100 +0,0 @@
|
||||
"""Adds config flow for HACS."""
|
||||
# pylint: disable=dangerous-default-value
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from aiogithubapi import AIOGitHub, AIOGitHubException, AIOGitHubAuthentication
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for HACS."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self._errors = {}
|
||||
|
||||
async def async_step_user(self, user_input={}):
|
||||
"""Handle a flow initialized by the user."""
|
||||
self._errors = {}
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
if self.hass.data.get(DOMAIN):
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
if user_input is not None:
|
||||
if await self._test_token(user_input["token"]):
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
self._errors["base"] = "auth"
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
async def _show_config_form(self, user_input):
|
||||
"""Show the configuration form to edit location data."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(hacs_base_config_schema(user_input)),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
return HacsOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_import(self, user_input):
|
||||
"""Import a config entry.
|
||||
Special type of import, we're not actually going to store any data.
|
||||
Instead, we're going to rely on the values that are in config file.
|
||||
"""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
return self.async_create_entry(title="configuration.yaml", data={})
|
||||
|
||||
async def _test_token(self, token):
|
||||
"""Return true if token is valid."""
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
client = AIOGitHub(token, session)
|
||||
await client.get_repo("hacs/org")
|
||||
return True
|
||||
except (AIOGitHubException, AIOGitHubAuthentication):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
class HacsOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""HACS config flow options handler."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize HACS options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
hacs_config_option_schema(self.config_entry.options)
|
||||
),
|
||||
)
|
||||
@@ -1,49 +0,0 @@
|
||||
"""HACS Configuration Schemas."""
|
||||
# pylint: disable=dangerous-default-value
|
||||
import voluptuous as vol
|
||||
from .const import LOCALE
|
||||
|
||||
# Configuration:
|
||||
TOKEN = "token"
|
||||
SIDEPANEL_TITLE = "sidepanel_title"
|
||||
SIDEPANEL_ICON = "sidepanel_icon"
|
||||
APPDAEMON = "appdaemon"
|
||||
PYTHON_SCRIPT = "python_script"
|
||||
THEME = "theme"
|
||||
|
||||
# Options:
|
||||
COUNTRY = "country"
|
||||
RELEASE_LIMIT = "release_limit"
|
||||
EXPERIMENTAL = "experimental"
|
||||
|
||||
|
||||
def hacs_base_config_schema(config: dict = {}) -> dict:
|
||||
"""Return a shcema configuration dict for HACS."""
|
||||
if not config:
|
||||
config = {
|
||||
TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
SIDEPANEL_TITLE: "Community",
|
||||
SIDEPANEL_ICON: "mdi:alpha-c-box",
|
||||
APPDAEMON: False,
|
||||
PYTHON_SCRIPT: False,
|
||||
THEME: False,
|
||||
}
|
||||
return {
|
||||
vol.Required(TOKEN, default=config.get(TOKEN)): str,
|
||||
vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str,
|
||||
vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str,
|
||||
vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool,
|
||||
vol.Optional(PYTHON_SCRIPT, default=config.get(PYTHON_SCRIPT)): bool,
|
||||
vol.Optional(THEME, default=config.get(THEME)): bool,
|
||||
}
|
||||
|
||||
|
||||
def hacs_config_option_schema(options: dict = {}) -> dict:
|
||||
"""Return a shcema for HACS configuration options."""
|
||||
if not options:
|
||||
options = {COUNTRY: "ALL", RELEASE_LIMIT: 5, EXPERIMENTAL: False}
|
||||
return {
|
||||
vol.Optional("country", default=options.get(COUNTRY)): vol.In(LOCALE),
|
||||
vol.Optional(RELEASE_LIMIT, default=options.get(RELEASE_LIMIT)): int,
|
||||
vol.Optional(EXPERIMENTAL, default=options.get(EXPERIMENTAL)): bool,
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
"""Constants for HACS"""
|
||||
NAME_LONG = "HACS (Home Assistant Community Store)"
|
||||
NAME_SHORT = "HACS"
|
||||
VERSION = "0.16.3"
|
||||
DOMAIN = "hacs"
|
||||
PROJECT_URL = "https://github.com/hacs/integration/"
|
||||
CUSTOM_UPDATER_LOCATIONS = [
|
||||
"{}/custom_components/custom_updater.py",
|
||||
"{}/custom_components/custom_updater/__init__.py",
|
||||
]
|
||||
|
||||
ISSUE_URL = f"{PROJECT_URL}issues"
|
||||
DOMAIN_DATA = f"{NAME_SHORT.lower()}_data"
|
||||
|
||||
ELEMENT_TYPES = ["integration", "plugin"]
|
||||
|
||||
IFRAME = {
|
||||
"title": "Community",
|
||||
"icon": "mdi:alpha-c-box",
|
||||
"url": "/community_overview",
|
||||
"path": "community",
|
||||
"require_admin": True,
|
||||
}
|
||||
|
||||
|
||||
# Messages
|
||||
CUSTOM_UPDATER_WARNING = """
|
||||
This cannot be used with custom_updater.
|
||||
To use this you need to remove custom_updater form {}
|
||||
"""
|
||||
|
||||
DEV_MODE = "You have 'dev' enabled for HACS, this is not intended for regular use, no support will be given if you break something."
|
||||
|
||||
STARTUP = f"""
|
||||
-------------------------------------------------------------------
|
||||
HACS (Home Assistant Community Store)
|
||||
|
||||
Version: {VERSION}
|
||||
This is a custom integration
|
||||
If you have any issues with this you need to open an issue here:
|
||||
https://github.com/hacs/integration/issues
|
||||
-------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
LOCALE = [
|
||||
"ALL",
|
||||
"AF",
|
||||
"AL",
|
||||
"DZ",
|
||||
"AS",
|
||||
"AD",
|
||||
"AO",
|
||||
"AI",
|
||||
"AQ",
|
||||
"AG",
|
||||
"AR",
|
||||
"AM",
|
||||
"AW",
|
||||
"AU",
|
||||
"AT",
|
||||
"AZ",
|
||||
"BS",
|
||||
"BH",
|
||||
"BD",
|
||||
"BB",
|
||||
"BY",
|
||||
"BE",
|
||||
"BZ",
|
||||
"BJ",
|
||||
"BM",
|
||||
"BT",
|
||||
"BO",
|
||||
"BQ",
|
||||
"BA",
|
||||
"BW",
|
||||
"BV",
|
||||
"BR",
|
||||
"IO",
|
||||
"BN",
|
||||
"BG",
|
||||
"BF",
|
||||
"BI",
|
||||
"KH",
|
||||
"CM",
|
||||
"CA",
|
||||
"CV",
|
||||
"KY",
|
||||
"CF",
|
||||
"TD",
|
||||
"CL",
|
||||
"CN",
|
||||
"CX",
|
||||
"CC",
|
||||
"CO",
|
||||
"KM",
|
||||
"CG",
|
||||
"CD",
|
||||
"CK",
|
||||
"CR",
|
||||
"HR",
|
||||
"CU",
|
||||
"CW",
|
||||
"CY",
|
||||
"CZ",
|
||||
"CI",
|
||||
"DK",
|
||||
"DJ",
|
||||
"DM",
|
||||
"DO",
|
||||
"EC",
|
||||
"EG",
|
||||
"SV",
|
||||
"GQ",
|
||||
"ER",
|
||||
"EE",
|
||||
"ET",
|
||||
"FK",
|
||||
"FO",
|
||||
"FJ",
|
||||
"FI",
|
||||
"FR",
|
||||
"GF",
|
||||
"PF",
|
||||
"TF",
|
||||
"GA",
|
||||
"GM",
|
||||
"GE",
|
||||
"DE",
|
||||
"GH",
|
||||
"GI",
|
||||
"GR",
|
||||
"GL",
|
||||
"GD",
|
||||
"GP",
|
||||
"GU",
|
||||
"GT",
|
||||
"GG",
|
||||
"GN",
|
||||
"GW",
|
||||
"GY",
|
||||
"HT",
|
||||
"HM",
|
||||
"VA",
|
||||
"HN",
|
||||
"HK",
|
||||
"HU",
|
||||
"IS",
|
||||
"IN",
|
||||
"ID",
|
||||
"IR",
|
||||
"IQ",
|
||||
"IE",
|
||||
"IM",
|
||||
"IL",
|
||||
"IT",
|
||||
"JM",
|
||||
"JP",
|
||||
"JE",
|
||||
"JO",
|
||||
"KZ",
|
||||
"KE",
|
||||
"KI",
|
||||
"KP",
|
||||
"KR",
|
||||
"KW",
|
||||
"KG",
|
||||
"LA",
|
||||
"LV",
|
||||
"LB",
|
||||
"LS",
|
||||
"LR",
|
||||
"LY",
|
||||
"LI",
|
||||
"LT",
|
||||
"LU",
|
||||
"MO",
|
||||
"MK",
|
||||
"MG",
|
||||
"MW",
|
||||
"MY",
|
||||
"MV",
|
||||
"ML",
|
||||
"MT",
|
||||
"MH",
|
||||
"MQ",
|
||||
"MR",
|
||||
"MU",
|
||||
"YT",
|
||||
"MX",
|
||||
"FM",
|
||||
"MD",
|
||||
"MC",
|
||||
"MN",
|
||||
"ME",
|
||||
"MS",
|
||||
"MA",
|
||||
"MZ",
|
||||
"MM",
|
||||
"NA",
|
||||
"NR",
|
||||
"NP",
|
||||
"NL",
|
||||
"NC",
|
||||
"NZ",
|
||||
"NI",
|
||||
"NE",
|
||||
"NG",
|
||||
"NU",
|
||||
"NF",
|
||||
"MP",
|
||||
"NO",
|
||||
"OM",
|
||||
"PK",
|
||||
"PW",
|
||||
"PS",
|
||||
"PA",
|
||||
"PG",
|
||||
"PY",
|
||||
"PE",
|
||||
"PH",
|
||||
"PN",
|
||||
"PL",
|
||||
"PT",
|
||||
"PR",
|
||||
"QA",
|
||||
"RO",
|
||||
"RU",
|
||||
"RW",
|
||||
"RE",
|
||||
"BL",
|
||||
"SH",
|
||||
"KN",
|
||||
"LC",
|
||||
"MF",
|
||||
"PM",
|
||||
"VC",
|
||||
"WS",
|
||||
"SM",
|
||||
"ST",
|
||||
"SA",
|
||||
"SN",
|
||||
"RS",
|
||||
"SC",
|
||||
"SL",
|
||||
"SG",
|
||||
"SX",
|
||||
"SK",
|
||||
"SI",
|
||||
"SB",
|
||||
"SO",
|
||||
"ZA",
|
||||
"GS",
|
||||
"SS",
|
||||
"ES",
|
||||
"LK",
|
||||
"SD",
|
||||
"SR",
|
||||
"SJ",
|
||||
"SZ",
|
||||
"SE",
|
||||
"CH",
|
||||
"SY",
|
||||
"TW",
|
||||
"TJ",
|
||||
"TZ",
|
||||
"TH",
|
||||
"TL",
|
||||
"TG",
|
||||
"TK",
|
||||
"TO",
|
||||
"TT",
|
||||
"TN",
|
||||
"TR",
|
||||
"TM",
|
||||
"TC",
|
||||
"TV",
|
||||
"UG",
|
||||
"UA",
|
||||
"AE",
|
||||
"GB",
|
||||
"US",
|
||||
"UM",
|
||||
"UY",
|
||||
"UZ",
|
||||
"VU",
|
||||
"VE",
|
||||
"VN",
|
||||
"VG",
|
||||
"VI",
|
||||
"WF",
|
||||
"EH",
|
||||
"YE",
|
||||
"ZM",
|
||||
"ZW",
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
"""HACS Startup constrains."""
|
||||
# pylint: disable=bad-continuation
|
||||
import os
|
||||
import json
|
||||
|
||||
from .const import CUSTOM_UPDATER_LOCATIONS, CUSTOM_UPDATER_WARNING
|
||||
from .helpers.misc import version_is_newer_than_version
|
||||
|
||||
|
||||
def check_constans(hacs):
|
||||
"""Check HACS constrains."""
|
||||
if not constrain_custom_updater(hacs):
|
||||
return False
|
||||
if not constrain_version(hacs):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def constrain_custom_updater(hacs):
|
||||
"""Check if custom_updater exist."""
|
||||
for location in CUSTOM_UPDATER_LOCATIONS:
|
||||
if os.path.exists(location.format(hacs.system.config_path)):
|
||||
msg = CUSTOM_UPDATER_WARNING.format(
|
||||
location.format(hacs.system.config_path)
|
||||
)
|
||||
hacs.logger.critical(msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def constrain_version(hacs):
|
||||
"""Check if the version is valid."""
|
||||
with open(
|
||||
f"{hacs.system.config_path}/custom_components/hacs/manifest.json", "r"
|
||||
) as read:
|
||||
manifest = json.loads(read.read())
|
||||
|
||||
# Check if HA is the required version.
|
||||
if version_is_newer_than_version(manifest["homeassistant"], hacs.system.ha_version):
|
||||
hacs.logger.critical(
|
||||
f"You need HA version {manifest['homeassistant']} or newer to use this integration."
|
||||
)
|
||||
return False
|
||||
return True
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Deprecated functions, should be fine to remove."""
|
||||
|
||||
|
||||
async def add_services(hacs):
|
||||
"""Add services."""
|
||||
# Service registration
|
||||
async def service_hacs_install(call):
|
||||
"""Install a repository."""
|
||||
repository = str(call.data["repository"])
|
||||
if repository not in hacs().store.repositories:
|
||||
hacs.logger.error("%s is not a konwn repository!", repository)
|
||||
return
|
||||
repository = hacs().store.repositories[repository]
|
||||
await repository.install()
|
||||
|
||||
async def service_hacs_register(call):
|
||||
"""register a repository."""
|
||||
repository = call.data["repository"]
|
||||
repository_type = call.data["repository_type"]
|
||||
if await hacs().is_known_repository(repository):
|
||||
hacs.logger.error("%s is already a konwn repository!", repository)
|
||||
return
|
||||
await hacs().register_new_repository(repository_type, repository)
|
||||
|
||||
async def service_hacs_load(call):
|
||||
"""register a repository."""
|
||||
from homeassistant.loader import async_get_custom_components
|
||||
|
||||
del hacs.hass.data["custom_components"]
|
||||
await async_get_custom_components(hacs.hass)
|
||||
|
||||
hacs.hass.services.async_register("hacs", "install", service_hacs_install)
|
||||
hacs.hass.services.async_register("hacs", "register", service_hacs_register)
|
||||
hacs.hass.services.async_register("hacs", "load", service_hacs_load)
|
||||
|
||||
|
||||
# This should probably be in a dedicated test "package"
|
||||
async def test_repositories(hacs):
|
||||
"""Test repositories."""
|
||||
await hacs().register_repository("ludeeus/theme-hacs", "theme")
|
||||
await hacs().register_repository("ludeeus/ps-hacs", "python_script")
|
||||
await hacs().register_repository("ludeeus/integration-hacs", "integration")
|
||||
await hacs().register_repository(
|
||||
"rgruebel/ha_zigbee2mqtt_networkmap", "integration"
|
||||
)
|
||||
await hacs().register_repository("ludeeus/ad-hacs", "appdaemon")
|
||||
await hacs().register_repository("jonkristian/entur-card", "plugin") # Dist
|
||||
await hacs().register_repository("kalkih/mini-media-player", "plugin") # Release
|
||||
await hacs().register_repository("custom-cards/monster-card", "plugin") # root
|
||||
Binary file not shown.
@@ -1,504 +0,0 @@
|
||||
.yaml {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
display: inline-flex;
|
||||
width: calc(100% - 20px);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.hacslist {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hacs-card-container {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.hacs-overview-container {
|
||||
width: 95%;
|
||||
margin-left: 2.5%;
|
||||
margin-right: 2.5%;
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hacs-form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.hacs-table {
|
||||
background-color: var(--paper-card-background-color);
|
||||
}
|
||||
|
||||
.hacs-content {
|
||||
position: absolute;
|
||||
top: 49px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.hacs-card {
|
||||
height: 100%;
|
||||
background-color: var(--paper-card-background-color) !important;
|
||||
padding: 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.overview {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
margin-left: 2.5%;
|
||||
margin-right: 2.5%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hacs-card-title {
|
||||
font-size: medium;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
tr.hacs-table-row.repository {
|
||||
border-bottom: 0px;
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
td.repository {
|
||||
width: 50%;
|
||||
padding: 0px;
|
||||
}
|
||||
td.repository.text {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
|
||||
select.hacs-select.repository {
|
||||
width: 100%;
|
||||
}
|
||||
.hacs-card-content {
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.card-status {
|
||||
font-size: 18px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.hacs-card-standalone {
|
||||
background-color: var(--paper-card-background-color) !important;
|
||||
color: var(--primary-text-color);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.installed {
|
||||
color: var(--hacs-status-installed);
|
||||
}
|
||||
|
||||
.pending-restart {
|
||||
color: var(--hacs-status-pending-restart);
|
||||
}
|
||||
|
||||
.pending-upgrade {
|
||||
color: var(--hacs-status-pending-update);
|
||||
}
|
||||
|
||||
.default {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.hacs-nav {
|
||||
background-color: var(--primary-color) !important;
|
||||
position: fixed;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.disabledaction {
|
||||
cursor: default;
|
||||
color: var(--primary-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hacs-bar {
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
.hacs-bar-background {
|
||||
background-color: var(--dark-primary-color) !important;
|
||||
}
|
||||
|
||||
.errorview {
|
||||
display: block;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.hacscolor {
|
||||
background-color: var(--paper-card-background-color) !important;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.hacsbutton {
|
||||
margin: 3%;
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.hacsadminbutton {
|
||||
margin: 1%;
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--primary-background-color) !important;
|
||||
color: var(--primary-text-color);
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="text"]:not(.browser-default):focus:not([readonly]) {
|
||||
border-bottom: 1px solid var(--primary-color);
|
||||
-webkit-box-shadow: 0 1px 0 0 var(--primary-color);
|
||||
box-shadow: 0 1px 0 0 var(--primary-color);
|
||||
}
|
||||
|
||||
.dropdown-list-item {
|
||||
color: var(--primary-color) !important;
|
||||
background-color: var(--paper-card-background-color) !important;
|
||||
}
|
||||
|
||||
a.dropdown-list-item:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.actionlink {
|
||||
color: var(--primary-color) !important
|
||||
}
|
||||
|
||||
.uninstall {
|
||||
float: right;
|
||||
color: var(--google-red-500) !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media all and (max-width: 550px) {
|
||||
select.hacs-select.custom_repository {
|
||||
width: 100%;
|
||||
}
|
||||
input#custom_url {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.actionlink {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
display: block;
|
||||
font-size: x-large;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.uninstall {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
.minigrid {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.card-action a:hover {
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
.codeinfo {
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
.hacs-select {
|
||||
display: initial !important;
|
||||
margin-left: 1%;
|
||||
width: 20%;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.hacs-badge {
|
||||
background-color: var(--hacs-badge-color);
|
||||
margin-right: 1%;
|
||||
font-size: 12px;
|
||||
padding: 0px 4px 2px 4px;
|
||||
color: var(--hacs-badge-text-color);
|
||||
-moz-border-radius: 10px 10px 10px 10px;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
tr.hacs-table-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* color vars*/
|
||||
:root {
|
||||
--hacs-status-installed: #126e15;
|
||||
--hacs-status-pending-restart: #a70000;
|
||||
--hacs-status-pending-update: #ffab40;
|
||||
--hacs-status-default: var(--primary-text-color);
|
||||
--hacs-badge-color: var(--primary-color);
|
||||
--hacs-badge-text-color: var(--primary-text-color);
|
||||
--primary-color: #03a9f4;
|
||||
--primary-text-color: #000000;
|
||||
--secondary-text-color: #424141;
|
||||
--primary-background-color: #fafafa;
|
||||
--paper-card-background-color: #ffffff;
|
||||
--dark-primary-color: #00405d;
|
||||
--google-red-500: #db4437;
|
||||
}
|
||||
|
||||
/* hug screens*/
|
||||
@media all and (min-width: 2000px) {
|
||||
.hacs-card-container {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* even bigger screens*/
|
||||
@media all and (min-width: 1500px) and (max-width: 1999px) {
|
||||
.hacs-card-container {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* bigger screens*/
|
||||
@media all and (min-width: 1000px) and (max-width: 1499px) {
|
||||
.hacs-card-container {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* big screens */
|
||||
@media all and (min-width: 550px) and (max-width: 999px) {
|
||||
.hacs-card-container {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
|
||||
#main::-webkit-scrollbar-track
|
||||
{
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#main::-webkit-scrollbar
|
||||
{
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
#main::-webkit-scrollbar-thumb
|
||||
{
|
||||
border-radius: 10px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* smal screens */
|
||||
@media all and (max-width: 550px) {
|
||||
body {
|
||||
overflow: scroll !important;
|
||||
}
|
||||
|
||||
.minigrid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.smal-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hacs-content {
|
||||
overflow: scroll !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* laoder */
|
||||
|
||||
*.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
div.loading{
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(16, 16, 16, 0.5);
|
||||
}
|
||||
|
||||
@-webkit-keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-ms-keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-o-keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes uil-ring-anim {
|
||||
0% {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.uil-ring-css {
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
.uil-ring-css > div {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
border-radius: 80px;
|
||||
box-shadow: 0 6px 0 0 var(--primary-color);
|
||||
-ms-animation: uil-ring-anim 1s linear infinite;
|
||||
-moz-animation: uil-ring-anim 1s linear infinite;
|
||||
-webkit-animation: uil-ring-anim 1s linear infinite;
|
||||
-o-animation: uil-ring-anim 1s linear infinite;
|
||||
animation: uil-ring-anim 1s linear infinite;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copy active HA theme
|
||||
document.getElementsByTagName("html").item(0).setAttribute("style", parent.document.getElementsByTagName("html").item(0).style.cssText)
|
||||
|
||||
// Copy yhe content of the Lovelace example to the clipboard.
|
||||
function CopyToLovelaceExampleToClipboard() {
|
||||
window.getSelection().selectAllChildren(document.getElementById("LovelaceExample"));
|
||||
document.execCommand("copy");
|
||||
document.getSelection().empty()
|
||||
document.getElementById('lovelacecopy').style.color = 'forestgreen';
|
||||
}
|
||||
|
||||
// Show progress bar
|
||||
function ShowProgressBar() {
|
||||
document.getElementById('progressbar').style.display = 'block';
|
||||
}
|
||||
|
||||
// Hard reloading
|
||||
function HardReload() {
|
||||
parent.location.reload(true)
|
||||
}
|
||||
|
||||
// Searchbar
|
||||
function Search() {
|
||||
var input = document.getElementById("Search");
|
||||
if (input) {
|
||||
var filter = input.value.toLowerCase();
|
||||
var nodes = document.getElementsByClassName('hacs-card');
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].innerHTML.toLowerCase().includes(filter)) {
|
||||
nodes[i].style.display = "block";
|
||||
} else {
|
||||
nodes[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
var nodes = document.getElementsByClassName('hacs-table-row');
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].innerHTML.toLowerCase().includes(filter)) {
|
||||
nodes[i].style.display = "table-row";
|
||||
} else {
|
||||
nodes[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Dropdown
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var elems = document.querySelectorAll('.dropdown-trigger');
|
||||
M.Dropdown.init(elems, { hover: false, constrainWidth: false });
|
||||
});
|
||||
|
||||
// Modal
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var elems = document.querySelectorAll('.modal');
|
||||
M.Modal.init(elems, {});
|
||||
});
|
||||
|
||||
|
||||
// Loader
|
||||
function toggleLoading() {
|
||||
var loadingOverlay = document.querySelector('.loading');
|
||||
loadingOverlay.classList.remove('hidden')
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
|
||||
// Check if we can reload
|
||||
function sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
}
|
||||
function CheckIfWeCanReload() {
|
||||
var data = true;
|
||||
const hacsrequest = new XMLHttpRequest()
|
||||
hacsrequest.open('GET', '/hacs_task', true)
|
||||
hacsrequest.onload = function () {
|
||||
data = JSON.parse(this.response)
|
||||
data = data["task"]
|
||||
if (!data) {
|
||||
console.log("Background task is no longer running, reloading in 5s...")
|
||||
sleep(5000).then(() => {
|
||||
location.reload()
|
||||
});
|
||||
}
|
||||
}
|
||||
hacsrequest.send()
|
||||
}
|
||||
|
||||
function IsTaskRunning() {
|
||||
let retval = false;
|
||||
let disp = document.getElementsByClassName("progress")
|
||||
if (disp) {
|
||||
disp = disp.item(0).style.display;
|
||||
if (disp == "block") {
|
||||
retval = true
|
||||
} else {
|
||||
retval = false
|
||||
}
|
||||
}
|
||||
return retval
|
||||
}
|
||||
|
||||
window.setInterval(function () {
|
||||
var running = false;
|
||||
running = IsTaskRunning();
|
||||
if (running) {
|
||||
CheckIfWeCanReload();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,51 +0,0 @@
|
||||
{% set progressbar = 'block' if hacs.system.status.background_task else 'none' %}
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/materialize.min.css.gz">
|
||||
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/all.min.css.gz">
|
||||
<script src="/hacsweb/{{ hacs.token }}/static/materialize.min.js.gz"></script>
|
||||
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/static/hacs.css">
|
||||
<script src="/hacsweb/{{ hacs.token }}/static/hacs.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar-fixed">
|
||||
<nav class="nav-extended hacs-nav">
|
||||
<div class="nav-content">
|
||||
<ul class="right tabs tabs-transparent">
|
||||
<li class="tab {{ 'active' if location == 'overview' }}"><a href="/hacsweb/{{ hacs.token }}/overview?timestamp={{ timestamp }}">Overview</a></li>
|
||||
<li class="tab {{ 'active' if location == 'store' }}"><a href="/hacsweb/{{ hacs.token }}/store?timestamp={{ timestamp }}">Store</a></li>
|
||||
<li class="tab right {{ 'active' if location == 'settings' }}"><a href="/hacsweb/{{ hacs.token }}/settings?timestamp={{ timestamp }}">Settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="main" class="hacs-content">
|
||||
<div class="progressbar" style="display: {{ progressbar }}">
|
||||
<p>Background task running, this page will reload when it's done.</p>
|
||||
<div class="progress hacs-bar-background" id="progressbar">
|
||||
<div class="indeterminate hacs-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading hidden">
|
||||
<div class='uil-ring-css' style='transform:scale(0.79);'>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if message %}
|
||||
{% include 'message.html' with context %}
|
||||
{% endif %}
|
||||
{% set hacs_repo = hacs.get_by_name("hacs/integration") %}
|
||||
{% if hacs_repo.pending_restart %}
|
||||
{% set message = "The frontend of HACS has been disabled until you restart Home Assistant." %}
|
||||
{% include 'message.html' with context %}
|
||||
{% else %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,36 +0,0 @@
|
||||
<div class='hacs-overview-container'>
|
||||
<h5>{{ displaytype }}</h5>
|
||||
<div class='hacs-card-container'>
|
||||
{% for repository in repositories.repositories %}
|
||||
<a href="/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}"
|
||||
class="hacs-card" onclick="toggleLoading()">
|
||||
<div class="hacs-card overview">
|
||||
<div id="statusicon">
|
||||
|
||||
</div>
|
||||
<div id="title">
|
||||
<input type="hidden" name="author" value="{{repository.information.authors}}">
|
||||
<input type="hidden" name="topics" value="{{repository.information.topics}}">
|
||||
<input type="hidden" name="full_name" value="{{repository.information.full_name}}">
|
||||
<span class="hacs-card-title">
|
||||
{% if location == 'store' and repository.status.new %}
|
||||
<span title="{{ repository.display_status_description }}" class='hacs-badge'>NEW</span>
|
||||
{% else %}
|
||||
<i title="{{ repository.display_status_description }}"
|
||||
class='fas fa-cube card-status {{repository.display_status}}'></i>
|
||||
{% endif %}
|
||||
{{repository.display_name}}
|
||||
</span>
|
||||
</div>
|
||||
<div id="description">
|
||||
<span class="hacs-card-content">
|
||||
<p>{{repository.information.description}}</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</br>
|
||||
</br>
|
||||
@@ -1,47 +0,0 @@
|
||||
{% set repositories = namespace(repositories=[]) %}
|
||||
{% for repository in hacs.sorted_by_name %}
|
||||
{% if repository.status.new %}
|
||||
{% set repositories.repositories = repositories.repositories+[repository] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if repositories.repositories %}
|
||||
<div class='hacs-overview-container'>
|
||||
<h5>NEW REPOSITORIES (ALL)</h5>
|
||||
<form action="/hacsapi/{{ hacs.token }}/remove_new_flag" name="remove_new_flag" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<input type="hidden" name="return" value="store">
|
||||
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
|
||||
onclick="toggleLoading();document.getElementsByName('remove_new_flag')[0].submit()" style="width: 95%;">
|
||||
Remove the NEW flag for all new items
|
||||
</a>
|
||||
</form>
|
||||
<div class='hacs-card-container'>
|
||||
{% for repository in repositories.repositories %}
|
||||
<a href="/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}"
|
||||
class="hacs-card" onclick="toggleLoading()">
|
||||
<div class="hacs-card overview">
|
||||
<div id="statusicon">
|
||||
|
||||
</div>
|
||||
<div id="title">
|
||||
<span class="hacs-card-title">
|
||||
<span class="hacs-badge">
|
||||
{{repository.information.category | upper}}
|
||||
</span>
|
||||
</br></br>
|
||||
{{repository.display_name}}
|
||||
</span>
|
||||
</div>
|
||||
<div id="description">
|
||||
<span class="hacs-card-content">
|
||||
<p>{{repository.information.description}}</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</br>
|
||||
</br>
|
||||
{% endif %}
|
||||
@@ -1,13 +0,0 @@
|
||||
<div class='container'>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<div class="card-panel orange darken-4">
|
||||
<div class="card-content white-text">
|
||||
<span>
|
||||
{{message}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,33 +0,0 @@
|
||||
<div id="modal_upgrade_all" class="modal hacscolor">
|
||||
<div class="modal-content">
|
||||
<h5>Pending Upgrades</h5>
|
||||
{% for repository in hacs.repositories %}
|
||||
{% if repository.pending_upgrade %}
|
||||
<p>- {{repository.information.name}} (
|
||||
{{repository.display_installed_version}}
|
||||
->
|
||||
{{repository.display_available_version}}
|
||||
)</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</br>
|
||||
<p>
|
||||
Be careful using this feature, elements may contain breaking changes,
|
||||
make sure you read the release notes for all the elements in the list above.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer hacscolor">
|
||||
{% if hacs.system.status.background_task %}
|
||||
<p>Background task is running, batch upgrade is disabled.</p>
|
||||
{% else %}
|
||||
<form action="/hacsapi/{{ hacs.token }}/repositories_upgrade_all" name="upgrade_all" method="post"
|
||||
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<a href="#" onclick="toggleLoading();document.getElementsByName('upgrade_all')[0].submit()"
|
||||
class='waves-effect waves-light btn hacsbutton'
|
||||
style="background-color: var(--google-red-500) !important; font-weight: bold;">
|
||||
UPGRADE ALL
|
||||
</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
<div id="haversion" class="modal hacscolor">
|
||||
<div class="modal-content">
|
||||
<h5>Unsupported Home Assistant version</h5>
|
||||
<p>
|
||||
You have version '{{hacs.system.ha_version}}' of Home Assistant, but version
|
||||
'{{repository.display_available_version}}' of '{{repository.display_name}}' require version
|
||||
'{{repository.repository_manifest.homeassistant}}' of Home Assistant, installation and upgrades are disabled
|
||||
for this
|
||||
integration untill you upgrade Home Assistant.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,77 +0,0 @@
|
||||
<!-- Used for both overview and store-->
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
{% if location == 'store' %}
|
||||
<div class='hacs-overview-container'>
|
||||
<input type="text" id="Search" onkeyup="Search()" placeholder="Please enter a search term.." title="Type in a name"
|
||||
autofocus style="color: var(--primary-text-color)">
|
||||
</div>
|
||||
{% if hacs.configuration.frontend_mode == "Grid" %}
|
||||
{% include 'grid_new.html' with context %}
|
||||
{% else %}
|
||||
{% include 'table_new.html' with context %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% for type in hacs.common.categories %}
|
||||
|
||||
{% if type == 'appdaemon' %}
|
||||
{% set displaytype = 'APPDAEMON APPS' %}
|
||||
{% elif type == 'python_script' %}
|
||||
{% set displaytype = 'PYTHON SCRIPTS' %}
|
||||
{% else %}
|
||||
{% set displaytype = (type + 's') | upper %}
|
||||
{% endif %}
|
||||
|
||||
{% set repositories = namespace(repositories=[]) %}
|
||||
|
||||
{% for repository in hacs.sorted_by_name %}
|
||||
{% set repo = namespace(include=True) %}
|
||||
|
||||
{% if repository.status.hide and repo.include %}
|
||||
{% set repo.include = False %}
|
||||
{% endif %}
|
||||
|
||||
{% if repository.information.category != type and repo.include %}
|
||||
{% set repo.include = False %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if location == 'overview' %}
|
||||
{% if not repository.status.installed and repo.include %}
|
||||
{% set repo.include = False %}
|
||||
{% if repository.information.uid == "172733314" %}
|
||||
{% set repo.include = True %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if repository.information.uid == "172733314" %}
|
||||
{% set repo.include = False %}
|
||||
{% endif %}
|
||||
{% if hacs.configuration.country and repo.include %}
|
||||
{% if repository.repository_manifest %}
|
||||
{% if repository.repository_manifest.country %}
|
||||
{% if hacs.configuration.country not in repository.repository_manifest.country %}
|
||||
{% set repo.include = False %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if repo.include %}
|
||||
{% set repositories.repositories = repositories.repositories+[repository] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if repositories.repositories %}
|
||||
{% if hacs.configuration.frontend_mode == "Grid" %}
|
||||
{% include 'grid.html' with context %}
|
||||
{% else %}
|
||||
{% include 'table.html' with context %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,93 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% if repository.pending_restart %}
|
||||
{% set message = "You need to restart (and potentially reconfigure) Home Assistant, for your last operation to be loaded." %}
|
||||
{% endif %}
|
||||
|
||||
{% if repository.information.category == "integration" and repository.status.first_install and repository.status.installed and repository.config_flow %}
|
||||
{% set message = "
|
||||
<b>Good news!</b></br>
|
||||
This integration supports config flows!</br>
|
||||
This means that you can go to 'Configuration' --> 'Integrations' --> '+' to configure it.</br>
|
||||
The best part if you do that, is that you do <b>not</b> need to restart Home Assistant.</br></br>
|
||||
<i>You may need to clear the cache on the integrations page before it shows in the list (+).</i>"%}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if message %}
|
||||
{% include 'message.html' with context %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<div class="card hacscolor">
|
||||
<div class="card-content">
|
||||
<span class="card-title">
|
||||
<b>{{repository.display_name}}</b>
|
||||
<a class='dropdown-trigger btn right' href='#' data-target='dropdown1'
|
||||
style="background-color: var(--primary-color); padding-top: 8px; height: 48px">
|
||||
<i class="fas fa-bars"></i>
|
||||
</a>
|
||||
|
||||
{% include 'repository/menu.html' with context %}
|
||||
</span>
|
||||
<p>{{repository.information.description}}</p></br>
|
||||
<table class="hacs-table">
|
||||
{% if repository.status.installed %}
|
||||
<tr class="hacs-table-row repository">
|
||||
<td class="repository">
|
||||
<b>Installed {{repository.display_version_or_commit}}:</b>
|
||||
</td>
|
||||
<td class="repository text">
|
||||
{{repository.display_installed_version}}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr class="hacs-table-row repository">
|
||||
{% if repository.releases.published_tags %}
|
||||
{% include 'repository/versionselect.html' with context %}
|
||||
{% else %}
|
||||
<td class="repository">
|
||||
<b>Available {{repository.display_version_or_commit}}:</b>
|
||||
</td>
|
||||
<td class="repository text">
|
||||
{{repository.display_available_version}}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if repository.status.selected_tag == repository.information.default_branch %}
|
||||
<tr class="hacs-table-row repository">
|
||||
<td class="repository">
|
||||
<b>Last updated:</b>
|
||||
</td>
|
||||
<td class="repository text">
|
||||
{{repository.information.last_updated}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% include 'repository/authors.html' with context %}
|
||||
</table>
|
||||
</div>
|
||||
{% include 'repository/buttons.html' with context %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<div class="card hacscolor">
|
||||
<div class="card-content" style="margin-top: -20px;">
|
||||
{{ repository.information.additional_info }}
|
||||
{% include 'repository/note.html' with context %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% if repository.information.authors %}
|
||||
{% if repository.information.category == "integration" %}
|
||||
<tr class="hacs-table-row repository">
|
||||
<td class="repository">
|
||||
<b>Author(s):</b>
|
||||
</td>
|
||||
<td class="repository text">
|
||||
{% for author in repository.information.authors %}
|
||||
{% if "@" in author %}
|
||||
{% set author = author.split("@")[-1] %}
|
||||
{% endif %}
|
||||
<a rel='noreferrer' href='https://github.com/{{author}}' target='_blank'
|
||||
style='color: var(--primary-color) !important; margin: 2'> @{{author}}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr class="hacs-table-row repository">
|
||||
<td class="repository">
|
||||
<b>Author:</b>
|
||||
</td>
|
||||
<td class="repository text">
|
||||
{{repository.information.authors}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -1,50 +0,0 @@
|
||||
{% if not repository.can_install %}
|
||||
{% include 'modal/wrong_ha_version.html' with context %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card-action">
|
||||
{% if repository.can_install %}
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_install" name="install" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<a class="actionlink" href="#" onclick="toggleLoading();document.getElementsByName('install')[0].submit()">
|
||||
{{repository.main_action}}
|
||||
</a>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="modal-trigger actionlink" href="#haversion">
|
||||
{{repository.main_action}}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if repository.pending_upgrade and repository.display_version_or_commit == "version" %}
|
||||
<a class="actionlink" rel='noreferrer'
|
||||
href='https://github.com/{{repository.information.full_name}}/releases/{{repository.available_version}}'
|
||||
target='_blank'>
|
||||
CHANGELOG
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="actionlink" rel='noreferrer' href='https://github.com/{{repository.information.full_name}}' target='_blank'>
|
||||
repository
|
||||
</a>
|
||||
|
||||
{% if repository.status.installed and repository.information.category == "plugin" %}
|
||||
{% set name = repository.information.full_name.split("/")[-1] %}
|
||||
<a class="actionlink" href='/community_plugin/{{name}}/{{repository.information.file_name}}' target='_blank'>
|
||||
OPEN PLUGIN
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if repository.status.installed and repository.information.uid != "172733314" %}
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_uninstall" name="uninstall" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<a class="actionlink uninstall" href='#'
|
||||
onclick="toggleLoading();document.getElementsByName('uninstall')[0].submit()">
|
||||
UNINSTALL
|
||||
</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
@@ -1,61 +0,0 @@
|
||||
<ul id='dropdown1' class='dropdown-content'>
|
||||
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_update" name="update" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<li>
|
||||
<a class="dropdown-list-item" href='#' onclick="toggleLoading();document.getElementsByName('update')[0].submit()">
|
||||
Reload
|
||||
</a>
|
||||
</li>
|
||||
</form>
|
||||
|
||||
{% if repository.display_version_or_commit == "version" %}
|
||||
{% set hide_show = "hide" if repository.status.show_beta else "show" %}
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_beta_{{hide_show}}" name="beta" method="post"
|
||||
accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<li>
|
||||
<a class="dropdown-list-item" href='#' onclick="toggleLoading();document.getElementsByName('beta')[0].submit()">
|
||||
{{hide_show | title}} beta
|
||||
</a>
|
||||
</li>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if not repository.status.installed %}
|
||||
{% if not repository.custom %}
|
||||
{% if repository.information.uid != "172733314" %}
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_hide" name="repository_hide" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<li>
|
||||
<a class="dropdown-list-item" href='#'
|
||||
onclick="toggleLoading();document.getElementsByName('repository_hide')[0].submit()">
|
||||
Hide
|
||||
</a>
|
||||
</li>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
<a class="dropdown-list-item" rel='noreferrer'
|
||||
href="https://github.com/{{repository.information.full_name}}/issues/" target="_blank">
|
||||
Open issue
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
{% if repository.information.uid != "172733314" %}
|
||||
<li>
|
||||
<a class="dropdown-list-item" rel='noreferrer'
|
||||
href="https://github.com/hacs/integration/issues/new?title={{repository.information.full_name}}&labels=flag&assignee=ludeeus&template=flag.md"
|
||||
target="_blank">
|
||||
Flag this
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
@@ -1,42 +0,0 @@
|
||||
{% if repository.additional_info %}
|
||||
</br>
|
||||
<hr></br>
|
||||
{% endif %}
|
||||
|
||||
{% if repository.information.category == "appdaemon" %}
|
||||
|
||||
<i>
|
||||
When installed, this will be located in '{{repository.content.path.local}}',
|
||||
you still need to add it to your 'apps.yaml' file.
|
||||
</i>
|
||||
|
||||
{% elif repository.information.category == "integration" %}
|
||||
|
||||
<i>
|
||||
When installed, this will be located in '{{repository.content.path.local}}',
|
||||
you still need to add it to your 'configuration.yaml' file.
|
||||
</i>
|
||||
|
||||
{% elif repository.information.category == "plugin" %}
|
||||
{% set name = repository.information.full_name.split("/")[-1] %}
|
||||
|
||||
<i>
|
||||
When installed, this will be located in '{{repository.content.path.local }}',
|
||||
you still need to add it to your lovelace configuration ('ui-lovelace.yaml' or the raw UI config editor).
|
||||
</i>
|
||||
</br></br>
|
||||
<i>
|
||||
When you add this to your configuration use this:
|
||||
</i></br>
|
||||
<pre id="LovelaceExample" class="yaml">
|
||||
- url: /community_plugin/{{name}}/{{repository.information.file_name}}
|
||||
{%- if repository.information.javascript_type %}
|
||||
type: {{repository.information.javascript_type}}
|
||||
{%- else %}
|
||||
type: Could not determine the type of this plugin, check the repository.
|
||||
{%- endif -%}
|
||||
</pre>
|
||||
<a title="Copy content to clipboard" id="lovelacecopy" onclick="CopyToLovelaceExampleToClipboard()">
|
||||
<i class="fa fa-copy"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -1,24 +0,0 @@
|
||||
{% set tags = repository.releases.published_tags | sort(True) %}
|
||||
{% set tags = tags + [repository.information.default_branch] %}
|
||||
|
||||
|
||||
{% if repository.status.selected_tag %}
|
||||
{% set selected_tag = repository.status.selected_tag %}
|
||||
{% else %}
|
||||
{% set selected_tag = repository.versions.available %}
|
||||
{% endif %}
|
||||
<td class="repository">
|
||||
<b>Available versions:</b>
|
||||
</td>
|
||||
<td class="repository">
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_select_tag" name="selected_tag" method="post"
|
||||
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<select name="selected_tag" class="hacs-select repository"
|
||||
onchange="toggleLoading();document.getElementsByName('selected_tag')[0].submit()">
|
||||
{% for tag in tags %}
|
||||
<option {{'selected' if selected_tag == tag else ''}} class='hacscolor' value='{{tag}}'>{{tag}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
{% if hacs.configuration.dev %}
|
||||
{% set message = "You have 'dev' enabled for HACS, this is not intended for regular use, no support will be given if you break something." %}
|
||||
{% include 'message.html' with context %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'modal/upgrade_all.html' with context %}
|
||||
|
||||
|
||||
{% include 'settings/hacs_info.html' with context %}
|
||||
{% include 'settings/buttons.html' with context %}
|
||||
{% include 'settings/custom_repositories.html' with context %}
|
||||
{% include 'settings/hidden_repositories.html' with context %}
|
||||
|
||||
|
||||
{% if hacs.configuration.dev and not hacs.system.status.background_task %}
|
||||
{% include 'settings/developer.html' with context %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,26 +0,0 @@
|
||||
<div class='hacs-overview-container minigrid'>
|
||||
|
||||
<form action="/hacsapi/{{ hacs.token }}/repositories_reload" name="reload" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
|
||||
onclick="toggleLoading();document.getElementsByName('reload')[0].submit()">
|
||||
RELOAD DATA
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<a href="#modal_upgrade_all" class="waves-effect waves-light btn modal-trigger hacsbutton"
|
||||
style="background-color: var(--google-red-500) !important; font-weight: bold;">
|
||||
UPGRADE ALL
|
||||
</a>
|
||||
|
||||
<a href='https://github.com/hacs/integration' rel='noreferrer' class='waves-effect waves-light btn hacsbutton'
|
||||
target="_blank">
|
||||
HACS REPO
|
||||
</a>
|
||||
|
||||
<a href='https://github.com/hacs/integration/issues' rel='noreferrer'
|
||||
class='waves-effect waves-light btn hacsbutton' target="_blank">
|
||||
OPEN ISSUE
|
||||
</a>
|
||||
|
||||
</div>
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="row">
|
||||
<ul class="collection with-header hacslist">
|
||||
<li class="collection-header hacscolor hacslist">
|
||||
<h5>CUSTOM REPOSITORIES</h5>
|
||||
</li>
|
||||
{% if hacs.system.status.background_task %}
|
||||
<li class="collection-item hacscolor hacslist">
|
||||
Loading custom repositories...
|
||||
</li>
|
||||
{% else %}
|
||||
{% for repository in hacs.sorted_by_repository_name %}
|
||||
{% if repository.custom %}
|
||||
<li class="collection-item hacscolor hacslist">
|
||||
<div>
|
||||
<a href="/hacsweb/{{ hacs.token }}/repository/{{repository.information.uid}}">
|
||||
<span class="hacs-badge">
|
||||
{{repository.information.category | upper}}
|
||||
</span>
|
||||
{{repository.information.full_name }}
|
||||
</a>
|
||||
{% if repository.status.installed %}
|
||||
<i title="Remove is not possible when installed."
|
||||
class="secondary-content fas fa-trash-alt disabledaction"></i>
|
||||
{% else %}
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_remove" name="remove" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<a class="secondary-content" href="#"
|
||||
onclick="toggleLoading();document.getElementsByName('remove')[0].submit()"
|
||||
style=" color: var(--primary-color)">
|
||||
<i title="Remove." class="fas fa-trash-alt"></i>
|
||||
</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
{% if not hacs.system.status.background_task %}
|
||||
<form class="minigrid" action="/hacsapi/{{ hacs.token }}/repository_register" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded">
|
||||
<input class="custom_repository" id="custom_url" type="text" name="custom_url"
|
||||
placeholder=" ADD CUSTOM REPOSITORY" style="width: 70%; color: var(--primary-text-color)">
|
||||
<select name="repository_type" class="hacs-select custom_repository">
|
||||
<option disabled selected value>Category</option>
|
||||
{% for element_type in hacs.common.categories | sort %}
|
||||
<option class='hacscolor' value='{{element_type}}'>{{element_type | title}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button class="btn waves-effect waves-light right" type="submit" name="add" onclick="toggleLoading()"
|
||||
style="background-color: var(--primary-color); height: 44px;">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="hacs-card-standalone">
|
||||
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/remove_new_flag" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
|
||||
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
|
||||
REMOVE FLAGS
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
{% for repository in hacs.store.frontend|sort(attribute='name') %}
|
||||
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/devrepository" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
|
||||
<input type="hidden" name="repository" value="{{repository.information.uid}}">
|
||||
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
|
||||
{{ repository.name }}
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="hacs-card-standalone">
|
||||
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/set_ha_version_action" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
|
||||
<input class="hacscolor" type="text" name="ha_version" value="{{hacs.store.ha_version}}">
|
||||
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
|
||||
SET
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="hacs-card-standalone">
|
||||
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/dev_template" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
|
||||
<input class="hacscolor" type="text" name="repository_id" value="{{ hacs.dev_template_id }}">
|
||||
<textarea class="hacscolor" name="template" style="width:100%;height:250px;"></textarea></br>
|
||||
<button name="set" class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
|
||||
TEST
|
||||
</button>
|
||||
<button name="clear" class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
|
||||
CLEAR
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if hacs.dev_template %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="hacs-card-standalone">
|
||||
{{ hacs.dev_template }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,8 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="hacs-card-standalone">
|
||||
<label>{{hacs.token}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,19 +0,0 @@
|
||||
<div class="hacs-overview-container hacs-color minigrid" style="margin-top: 30px;">
|
||||
<div class="hacs-card-standalone">
|
||||
<h5 style="margin-left: 5px;">DEVELOPER TOOLS</h5>
|
||||
{% for button in [
|
||||
"remove_new_flag",
|
||||
"repositories",
|
||||
"set_ha_version",
|
||||
"token",
|
||||
"template_test"
|
||||
] %}
|
||||
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/devview" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
|
||||
<input type="hidden" name="view" value="{{button}}">
|
||||
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
|
||||
{{ button.replace("_"," ")}}
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
{% if hacs.get_by_id("172733314").pending_restart %}
|
||||
{% set pending_restart = " <b>(RESTART PENDING!)</b>" %}
|
||||
{% else %}
|
||||
{% set pending_restart = "" %}
|
||||
{% endif %}
|
||||
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="hacs-card-standalone">
|
||||
<h5>HACS (Home Assistant Community Store)</h5>
|
||||
<table class="hacs-table">
|
||||
<tr class="hacs-table-row repository">
|
||||
<td class="repository">
|
||||
<b>Version:</b>
|
||||
</td>
|
||||
<td class="repository text">
|
||||
{{hacs.version}}{{pending_restart}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hacs-table-row repository">
|
||||
<td class="repository">
|
||||
<b>Display:</b>
|
||||
</td>
|
||||
<td class="repository">
|
||||
<form action="/hacsapi/{{ hacs.token }}/frontend_mode" name="overview_display" method="post"
|
||||
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<select name="view_type" class="hacs-select repository"
|
||||
onchange="toggleLoading();document.getElementsByName('overview_display')[0].submit()">
|
||||
{% for displayoption in ["Grid", "Table"] %}
|
||||
<option {{"selected" if hacs.configuration.frontend_mode == displayoption else ""}} class="hacscolor"
|
||||
value="{{displayoption}}">{{displayoption}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
{% set repositories = namespace(hidden=False) %}
|
||||
{% for repository in hacs.repositories_list_repo %}
|
||||
{% if repository.hide %}
|
||||
{% set repositories.hidden = True %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if repositories.hidden %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="row">
|
||||
<ul class="collection with-header hacslist">
|
||||
<li class="collection-header hacscolor hacslist">
|
||||
<h5>HIDDEN REPOSITORIES</h5>
|
||||
</li>
|
||||
{% if hacs.system.status.background_task %}
|
||||
<li class="collection-item hacscolor hacslist">
|
||||
Loading hidden repositories...
|
||||
</li>
|
||||
{% else %}
|
||||
{% for repository in hacs.repositories_list_repo %}
|
||||
{% if repository.hide %}
|
||||
<li class="collection-item hacscolor hacslist">
|
||||
<div>
|
||||
<form action="/hacsapi/{{ hacs.token }}/repository_unhide" name="remove" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
|
||||
<a href="#" onclick="toggleLoading();document.getElementsByName('remove')[0].submit()"
|
||||
style=" color: var(--primary-color)">
|
||||
<i title="Unhide" class="fas fa-plus-circle" style="padding-right: 8px"></i>
|
||||
</a>
|
||||
{{repository.repository_name}}
|
||||
<span class="hacs-badge">{{repository.repository_type}}</span>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,42 +0,0 @@
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="row">
|
||||
<h5>{{ displaytype }}</h5>
|
||||
<table class="hacs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Name</th>
|
||||
<th class="smal-hide">Description</th>
|
||||
{% if location == 'overview' %}
|
||||
<th class="smal-hide">Installed</th>
|
||||
<th class="smal-hide">Available</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for repository in repositories.repositories %}
|
||||
<tr class="hacs-table-row"
|
||||
onclick="toggleLoading();window.location='/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}';">
|
||||
<input type="hidden" name="author" value="{{repository.information.authors}}">
|
||||
<input type="hidden" name="topics" value="{{repository.information.topics}}">
|
||||
<input type="hidden" name="full_name" value="{{repository.information.full_name}}">
|
||||
<td title="{{ repository.display_status_description }}">
|
||||
{% if location == 'store' and repository.status.new %}
|
||||
<span style='margin-left: 25%' class='repository-list-badge hacs-badge'>NEW</span>
|
||||
{% else %}
|
||||
<i style='margin-left: 25%' class='fas fa-cube card-status {{repository.display_status}}'></i></td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{repository.display_name}}
|
||||
</td>
|
||||
<td class="hacs-card-content smal-hide">{{repository.information.description}}</td>
|
||||
{% if location == 'overview' %}
|
||||
<td class="smal-hide">{{repository.display_installed_version}}</td>
|
||||
<td class="smal-hide">{{repository.display_available_version}}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,44 +0,0 @@
|
||||
{% set repositories = namespace(repositories=[]) %}
|
||||
{% for repository in hacs.sorted_by_name %}
|
||||
{% if repository.status.new %}
|
||||
{% set repositories.repositories = repositories.repositories+[repository] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if repositories.repositories %}
|
||||
<div class='hacs-overview-container'>
|
||||
<div class="row">
|
||||
<h5>NEW REPOSITORIES (ALL)</h5>
|
||||
<form action="/hacsapi/{{ hacs.token }}/remove_new_flag" name="remove_new_flag" method="post" accept-charset="utf-8"
|
||||
enctype="application/x-www-form-urlencoded" class="hacs-form">
|
||||
<input type="hidden" name="return" value="store">
|
||||
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
|
||||
onclick="toggleLoading();document.getElementsByName('remove_new_flag')[0].submit()" style="width: 95%;">
|
||||
Remove the NEW flag for all new items
|
||||
</a>
|
||||
</form>
|
||||
<table class="hacs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Name</th>
|
||||
<th class="smal-hide">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for repository in repositories.repositories %}
|
||||
<tr class="hacs-table-row"
|
||||
onclick="toggleLoading();window.location='/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}';">
|
||||
<td>
|
||||
<span class="hacs-badge">{{repository.information.category | upper}}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{repository.display_name}}
|
||||
</td>
|
||||
<td class="hacs-card-content smal-hide">{{repository.information.description}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,391 +0,0 @@
|
||||
"""Initialize the HACS base."""
|
||||
# pylint: disable=unused-argument, bad-continuation
|
||||
import json
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
|
||||
from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit
|
||||
from integrationhelper import Logger
|
||||
|
||||
|
||||
from ..const import ELEMENT_TYPES
|
||||
from ..store import async_load_from_store, async_save_to_store
|
||||
|
||||
|
||||
class HacsStatus:
|
||||
"""HacsStatus."""
|
||||
|
||||
startup = False
|
||||
background_task = False
|
||||
reloading_data = False
|
||||
upgrading_all = False
|
||||
|
||||
|
||||
class HacsCommon:
|
||||
"""Common for HACS."""
|
||||
|
||||
categories = []
|
||||
blacklist = []
|
||||
default = []
|
||||
installed = []
|
||||
skip = []
|
||||
|
||||
|
||||
class System:
|
||||
"""System info."""
|
||||
|
||||
status = HacsStatus()
|
||||
config_path = None
|
||||
ha_version = None
|
||||
disabled = False
|
||||
|
||||
|
||||
class Developer:
|
||||
"""Developer settings/tools."""
|
||||
|
||||
template_id = "Repository ID"
|
||||
template_content = ""
|
||||
template_raw = ""
|
||||
|
||||
@property
|
||||
def devcontainer(self):
|
||||
"""Is it a devcontainer?"""
|
||||
import os
|
||||
|
||||
if "DEVCONTAINER" in os.environ:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Hacs:
|
||||
"""The base class of HACS, nested thoughout the project."""
|
||||
|
||||
token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}"
|
||||
hacsweb = f"/hacsweb/{token}"
|
||||
hacsapi = f"/hacsapi/{token}"
|
||||
repositories = []
|
||||
repo = None
|
||||
data_repo = None
|
||||
developer = Developer()
|
||||
data = None
|
||||
configuration = None
|
||||
logger = Logger("hacs")
|
||||
github = None
|
||||
hass = None
|
||||
version = None
|
||||
system = System()
|
||||
tasks = []
|
||||
common = HacsCommon()
|
||||
|
||||
@staticmethod
|
||||
def init(hass, github_token):
|
||||
"""Return a initialized HACS object."""
|
||||
return Hacs()
|
||||
|
||||
def get_by_id(self, repository_id):
|
||||
"""Get repository by ID."""
|
||||
try:
|
||||
for repository in self.repositories:
|
||||
if repository.information.uid == repository_id:
|
||||
return repository
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_by_name(self, repository_full_name):
|
||||
"""Get repository by full_name."""
|
||||
try:
|
||||
for repository in self.repositories:
|
||||
if repository.information.full_name == repository_full_name:
|
||||
return repository
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
return None
|
||||
|
||||
def is_known(self, repository_full_name):
|
||||
"""Return a bool if the repository is known."""
|
||||
for repository in self.repositories:
|
||||
if repository.information.full_name == repository_full_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def sorted_by_name(self):
|
||||
"""Return a sorted(by name) list of repository objects."""
|
||||
return sorted(self.repositories, key=lambda x: x.display_name)
|
||||
|
||||
@property
|
||||
def sorted_by_repository_name(self):
|
||||
"""Return a sorted(by repository_name) list of repository objects."""
|
||||
return sorted(self.repositories, key=lambda x: x.information.full_name)
|
||||
|
||||
async def register_repository(self, full_name, category, check=True):
|
||||
"""Register a repository."""
|
||||
from ..repositories.repository import RERPOSITORY_CLASSES
|
||||
|
||||
if full_name in self.common.skip:
|
||||
if full_name != "hacs/integration":
|
||||
self.logger.debug(f"Skipping {full_name}")
|
||||
return
|
||||
|
||||
if category not in RERPOSITORY_CLASSES:
|
||||
self.logger.error(f"{category} is not a valid repository category.")
|
||||
return False
|
||||
|
||||
repository = RERPOSITORY_CLASSES[category](full_name)
|
||||
if check:
|
||||
try:
|
||||
await repository.registration()
|
||||
if repository.validate.errors:
|
||||
self.common.skip.append(repository.information.full_name)
|
||||
if not self.system.status.startup:
|
||||
self.logger.error(f"Validation for {full_name} failed.")
|
||||
return repository.validate.errors
|
||||
repository.logger.info("Registration complete")
|
||||
except AIOGitHubException as exception:
|
||||
self.logger.debug(self.github.ratelimits.remaining)
|
||||
self.logger.debug(self.github.ratelimits.reset_utc)
|
||||
self.common.skip.append(repository.information.full_name)
|
||||
if not self.system.status.startup:
|
||||
self.logger.error(
|
||||
f"Validation for {full_name} failed with {exception}."
|
||||
)
|
||||
return exception
|
||||
self.hass.bus.async_fire(
|
||||
"hacs/repository",
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "registration",
|
||||
"repository": repository.information.full_name,
|
||||
"repository_id": repository.information.uid,
|
||||
},
|
||||
)
|
||||
self.repositories.append(repository)
|
||||
|
||||
async def startup_tasks(self):
|
||||
"""Tasks tha are started after startup."""
|
||||
self.system.status.background_task = True
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
self.logger.debug(self.github.ratelimits.remaining)
|
||||
self.logger.debug(self.github.ratelimits.reset_utc)
|
||||
|
||||
await self.handle_critical_repositories_startup()
|
||||
await self.handle_critical_repositories()
|
||||
await self.load_known_repositories()
|
||||
await self.clear_out_blacklisted_repositories()
|
||||
|
||||
self.tasks.append(
|
||||
async_track_time_interval(
|
||||
self.hass, self.recuring_tasks_installed, timedelta(minutes=30)
|
||||
)
|
||||
)
|
||||
self.tasks.append(
|
||||
async_track_time_interval(
|
||||
self.hass, self.recuring_tasks_all, timedelta(minutes=800)
|
||||
)
|
||||
)
|
||||
|
||||
self.system.status.startup = False
|
||||
self.system.status.background_task = False
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
await self.data.async_write()
|
||||
|
||||
async def handle_critical_repositories_startup(self):
|
||||
"""Handled critical repositories during startup."""
|
||||
alert = False
|
||||
critical = await async_load_from_store(self.hass, "critical")
|
||||
if not critical:
|
||||
return
|
||||
for repo in critical:
|
||||
if not repo["acknowledged"]:
|
||||
alert = True
|
||||
if alert:
|
||||
self.logger.critical("URGENT!: Check the HACS panel!")
|
||||
self.hass.components.persistent_notification.create(
|
||||
title="URGENT!", message="**Check the HACS panel!**"
|
||||
)
|
||||
|
||||
async def handle_critical_repositories(self):
|
||||
"""Handled critical repositories during runtime."""
|
||||
# Get critical repositories
|
||||
instored = []
|
||||
critical = []
|
||||
was_installed = False
|
||||
|
||||
try:
|
||||
critical = await self.data_repo.get_contents("critical")
|
||||
critical = json.loads(critical.content)
|
||||
except AIOGitHubException:
|
||||
pass
|
||||
|
||||
if not critical:
|
||||
self.logger.debug("No critical repositories")
|
||||
return
|
||||
|
||||
stored_critical = await async_load_from_store(self.hass, "critical")
|
||||
|
||||
for stored in stored_critical or []:
|
||||
instored.append(stored["repository"])
|
||||
|
||||
stored_critical = []
|
||||
|
||||
for repository in critical:
|
||||
self.common.blacklist.append(repository["repository"])
|
||||
repo = self.get_by_name(repository["repository"])
|
||||
|
||||
stored = {
|
||||
"repository": repository["repository"],
|
||||
"reason": repository["reason"],
|
||||
"link": repository["link"],
|
||||
"acknowledged": True,
|
||||
}
|
||||
|
||||
if repository["repository"] not in instored:
|
||||
if repo is not None and repo.installed:
|
||||
self.logger.critical(
|
||||
f"Removing repository {repository['repository']}, it is marked as critical"
|
||||
)
|
||||
was_installed = True
|
||||
stored["acknowledged"] = False
|
||||
# Uninstall from HACS
|
||||
repo.remove()
|
||||
await repo.uninstall()
|
||||
stored_critical.append(stored)
|
||||
|
||||
# Save to FS
|
||||
await async_save_to_store(self.hass, "critical", stored_critical)
|
||||
|
||||
# Resart HASS
|
||||
if was_installed:
|
||||
self.logger.critical("Resarting Home Assistant")
|
||||
self.hass.async_create_task(self.hass.async_stop(100))
|
||||
|
||||
async def recuring_tasks_installed(self, notarealarg=None):
|
||||
"""Recuring tasks for installed repositories."""
|
||||
self.logger.debug(
|
||||
"Starting recuring background task for installed repositories"
|
||||
)
|
||||
self.system.status.background_task = True
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
self.logger.debug(self.github.ratelimits.remaining)
|
||||
self.logger.debug(self.github.ratelimits.reset_utc)
|
||||
for repository in self.repositories:
|
||||
if repository.status.installed:
|
||||
try:
|
||||
await repository.update_repository()
|
||||
repository.logger.debug("Information update done.")
|
||||
except AIOGitHubException:
|
||||
self.system.status.background_task = False
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
await self.data.async_write()
|
||||
self.logger.debug(
|
||||
"Recuring background task for installed repositories done"
|
||||
)
|
||||
return
|
||||
await self.handle_critical_repositories()
|
||||
self.system.status.background_task = False
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
await self.data.async_write()
|
||||
self.logger.debug("Recuring background task for installed repositories done")
|
||||
|
||||
async def recuring_tasks_all(self, notarealarg=None):
|
||||
"""Recuring tasks for all repositories."""
|
||||
self.logger.debug("Starting recuring background task for all repositories")
|
||||
self.system.status.background_task = True
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
self.logger.debug(self.github.ratelimits.remaining)
|
||||
self.logger.debug(self.github.ratelimits.reset_utc)
|
||||
for repository in self.repositories:
|
||||
try:
|
||||
await repository.update_repository()
|
||||
repository.logger.debug("Information update done.")
|
||||
except AIOGitHubException:
|
||||
self.system.status.background_task = False
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
await self.data.async_write()
|
||||
self.logger.debug("Recuring background task for all repositories done")
|
||||
return
|
||||
await self.load_known_repositories()
|
||||
await self.clear_out_blacklisted_repositories()
|
||||
self.system.status.background_task = False
|
||||
await self.data.async_write()
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
|
||||
self.logger.debug("Recuring background task for all repositories done")
|
||||
|
||||
async def clear_out_blacklisted_repositories(self):
|
||||
"""Clear out blaclisted repositories."""
|
||||
need_to_save = False
|
||||
for repository in self.common.blacklist:
|
||||
if self.is_known(repository):
|
||||
repository = self.get_by_name(repository)
|
||||
if repository.status.installed:
|
||||
self.logger.error(
|
||||
f"You have {repository.information.full_name} installed with HACS, this repositroy has been blacklisted, please consider removing it."
|
||||
)
|
||||
else:
|
||||
need_to_save = True
|
||||
repository.remove()
|
||||
|
||||
if need_to_save:
|
||||
await self.data.async_write()
|
||||
|
||||
async def get_repositories(self):
|
||||
"""Return a list of repositories."""
|
||||
repositories = {}
|
||||
if self.configuration.dev:
|
||||
if self.developer.devcontainer:
|
||||
repositories = {
|
||||
"appdaemon": ["ludeeus/ad-hacs"],
|
||||
"integration": ["ludeeus/integration-hacs"],
|
||||
"plugin": ["maykar/compact-custom-header"],
|
||||
"python_script": ["ludeeus/ps-hacs"],
|
||||
"theme": ["ludeeus/theme-hacs"],
|
||||
}
|
||||
else:
|
||||
for category in self.common.categories:
|
||||
remote = await self.data_repo.get_contents(category)
|
||||
repositories[category] = json.loads(remote.content)
|
||||
if category == "plugin":
|
||||
org = await self.github.get_org_repos("custom-cards")
|
||||
for repo in org:
|
||||
repositories[category].append(repo.full_name)
|
||||
if category == "integration":
|
||||
org = await self.github.get_org_repos("custom-components")
|
||||
for repo in org:
|
||||
repositories[category].append(repo.full_name)
|
||||
if category == "theme":
|
||||
org = await self.github.get_org_repos(
|
||||
"home-assistant-community-themes"
|
||||
)
|
||||
for repo in org:
|
||||
repositories[category].append(repo.full_name)
|
||||
|
||||
for category in repositories:
|
||||
for repo in repositories[category]:
|
||||
if repo not in self.common.default:
|
||||
self.common.default.append(repo)
|
||||
return repositories
|
||||
|
||||
async def load_known_repositories(self):
|
||||
"""Load known repositories."""
|
||||
self.logger.info("Loading known repositories")
|
||||
blacklist = await self.data_repo.get_contents("blacklist")
|
||||
repositories = await self.get_repositories()
|
||||
|
||||
for item in json.loads(blacklist.content):
|
||||
if item not in self.common.blacklist:
|
||||
self.common.blacklist.append(item)
|
||||
|
||||
for category in repositories:
|
||||
for repo in repositories[category]:
|
||||
if repo in self.common.blacklist:
|
||||
continue
|
||||
if self.is_known(repo):
|
||||
continue
|
||||
try:
|
||||
await self.register_repository(repo, category)
|
||||
except (AIOGitHubException, AIOGitHubRatelimit):
|
||||
pass
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Backup."""
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from time import sleep
|
||||
|
||||
from integrationhelper import Logger
|
||||
|
||||
BACKUP_PATH = tempfile.gettempdir() + "/hacs_backup/"
|
||||
|
||||
|
||||
class Backup:
|
||||
"""Backup."""
|
||||
|
||||
def __init__(self, local_path, backup_path=BACKUP_PATH):
|
||||
"""initialize."""
|
||||
self.logger = Logger("hacs.backup")
|
||||
self.local_path = local_path
|
||||
self.backup_path = backup_path
|
||||
self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}"
|
||||
|
||||
def create(self):
|
||||
"""Create a backup in /tmp"""
|
||||
if not os.path.exists(self.local_path):
|
||||
return
|
||||
if os.path.exists(self.backup_path):
|
||||
shutil.rmtree(self.backup_path)
|
||||
while os.path.exists(self.backup_path):
|
||||
sleep(0.1)
|
||||
os.makedirs(self.backup_path, exist_ok=True)
|
||||
|
||||
try:
|
||||
if os.path.isfile(self.local_path):
|
||||
shutil.copyfile(self.local_path, self.backup_path_full)
|
||||
os.remove(self.local_path)
|
||||
else:
|
||||
shutil.copytree(self.local_path, self.backup_path_full)
|
||||
shutil.rmtree(self.local_path)
|
||||
while os.path.exists(self.local_path):
|
||||
sleep(0.1)
|
||||
self.logger.debug(
|
||||
f"Backup for {self.local_path}, created in {self.backup_path_full}"
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
def restore(self):
|
||||
"""Restore from backup."""
|
||||
if not os.path.exists(self.backup_path_full):
|
||||
return
|
||||
|
||||
if os.path.isfile(self.backup_path_full):
|
||||
if os.path.exists(self.local_path):
|
||||
os.remove(self.local_path)
|
||||
shutil.copyfile(self.backup_path_full, self.local_path)
|
||||
else:
|
||||
if os.path.exists(self.local_path):
|
||||
shutil.rmtree(self.local_path)
|
||||
while os.path.exists(self.local_path):
|
||||
sleep(0.1)
|
||||
shutil.copytree(self.backup_path_full, self.local_path)
|
||||
self.logger.debug(
|
||||
f"Restored {self.local_path}, from backup {self.backup_path_full}"
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup backup files."""
|
||||
if os.path.exists(self.backup_path):
|
||||
shutil.rmtree(self.backup_path)
|
||||
while os.path.exists(self.backup_path):
|
||||
sleep(0.1)
|
||||
self.logger.debug(f"Backup dir {self.backup_path} cleared")
|
||||
@@ -1,55 +0,0 @@
|
||||
"""HACS Configuration."""
|
||||
import attr
|
||||
|
||||
from custom_components.hacs.hacsbase.exceptions import HacsUserScrewupException
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class Configuration:
|
||||
"""Configuration class."""
|
||||
|
||||
# Main configuration:
|
||||
appdaemon_path: str = "appdaemon/apps/"
|
||||
appdaemon: bool = False
|
||||
config: dict = {}
|
||||
config_entry: dict = {}
|
||||
config_type: str = None
|
||||
dev: bool = False
|
||||
frontend_mode: str = "Grid"
|
||||
options: dict = {}
|
||||
plugin_path: str = "www/community/"
|
||||
python_script_path: str = "python_scripts/"
|
||||
python_script: bool = False
|
||||
sidepanel_icon: str = "mdi:alpha-c-box"
|
||||
sidepanel_title: str = "Community"
|
||||
theme_path: str = "themes/"
|
||||
theme: bool = False
|
||||
token: str = None
|
||||
|
||||
# Config options:
|
||||
country: str = "ALL"
|
||||
experimental: bool = False
|
||||
release_limit: int = 5
|
||||
|
||||
@staticmethod
|
||||
def from_dict(configuration: dict, options: dict):
|
||||
"""Set attributes from dicts."""
|
||||
if isinstance(options, bool) or isinstance(configuration.get("options"), bool):
|
||||
raise HacsUserScrewupException("Configuration is not valid.")
|
||||
|
||||
if options is None:
|
||||
options = {}
|
||||
|
||||
if not configuration:
|
||||
raise HacsUserScrewupException("Configuration is not valid.")
|
||||
|
||||
config = Configuration()
|
||||
|
||||
config.config = configuration
|
||||
config.options = options
|
||||
|
||||
for conf_type in [configuration, options]:
|
||||
for key in conf_type:
|
||||
setattr(config, key, conf_type[key])
|
||||
|
||||
return config
|
||||
@@ -1,10 +0,0 @@
|
||||
"""Constants for HACS"""
|
||||
# pylint: disable=unused-import
|
||||
STORAGE_VERSION = "6"
|
||||
STORENAME = "hacs"
|
||||
|
||||
# Messages
|
||||
NOT_SUPPORTED_HA_VERSION = "You have version '{}' of Home Assistant, but version '{}' of '{}' require version '{}' of Home Assistant, install and upgrades are disabled for this integration untill you upgrade Home Assistant."
|
||||
|
||||
|
||||
NO_ELEMENTS = "No elements to show, open the store to install some awesome stuff."
|
||||
@@ -1,215 +0,0 @@
|
||||
"""Data handler for HACS."""
|
||||
import os
|
||||
import json
|
||||
from integrationhelper import Logger
|
||||
from . import Hacs
|
||||
from .const import STORAGE_VERSION
|
||||
from ..const import VERSION
|
||||
from ..repositories.manifest import HacsManifest
|
||||
from ..store import async_save_to_store, async_load_from_store
|
||||
|
||||
|
||||
STORES = {
|
||||
"old": "hacs",
|
||||
"hacs": "hacs.hacs",
|
||||
"installed": "hacs.installed",
|
||||
"repositories": "hacs.repositories",
|
||||
}
|
||||
|
||||
|
||||
class HacsData(Hacs):
|
||||
"""HacsData class."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.logger = Logger("hacs.data")
|
||||
|
||||
def check_corrupted_files(self):
|
||||
"""Return True if one (or more) of the files are corrupted."""
|
||||
for store in STORES:
|
||||
path = f"{self.system.config_path}/.storage/{STORES[store]}"
|
||||
if os.path.exists(path):
|
||||
if os.stat(path).st_size == 0:
|
||||
# File is empty (corrupted)
|
||||
return True
|
||||
return False
|
||||
|
||||
def read(self, store):
|
||||
"""Return data from a store."""
|
||||
path = f"{self.system.config_path}/.storage/{STORES[store]}"
|
||||
content = None
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as storefile:
|
||||
content = storefile.read()
|
||||
content = json.loads(content)
|
||||
return content
|
||||
|
||||
async def async_write(self):
|
||||
"""Write content to the store files."""
|
||||
if self.system.status.background_task:
|
||||
return
|
||||
|
||||
self.logger.debug("Saving data")
|
||||
|
||||
# Hacs
|
||||
await async_save_to_store(
|
||||
self.hass, "hacs", {"view": self.configuration.frontend_mode}
|
||||
)
|
||||
|
||||
# Repositories
|
||||
content = {}
|
||||
for repository in self.repositories:
|
||||
if repository.repository_manifest is not None:
|
||||
repository_manifest = repository.repository_manifest.manifest
|
||||
else:
|
||||
repository_manifest = None
|
||||
content[repository.information.uid] = {
|
||||
"authors": repository.information.authors,
|
||||
"topics": repository.information.topics,
|
||||
"category": repository.information.category,
|
||||
"description": repository.information.description,
|
||||
"full_name": repository.information.full_name,
|
||||
"hide": repository.status.hide,
|
||||
"installed_commit": repository.versions.installed_commit,
|
||||
"installed": repository.status.installed,
|
||||
"last_commit": repository.versions.available_commit,
|
||||
"last_release_tag": repository.versions.available,
|
||||
"repository_manifest": repository_manifest,
|
||||
"name": repository.information.name,
|
||||
"new": repository.status.new,
|
||||
"selected_tag": repository.status.selected_tag,
|
||||
"show_beta": repository.status.show_beta,
|
||||
"version_installed": repository.versions.installed,
|
||||
}
|
||||
|
||||
await async_save_to_store(self.hass, "repositories", content)
|
||||
self.hass.bus.async_fire("hacs/repository", {})
|
||||
self.hass.bus.fire("hacs/config", {})
|
||||
|
||||
async def restore(self):
|
||||
"""Restore saved data."""
|
||||
hacs = {}
|
||||
repositories = {}
|
||||
|
||||
try:
|
||||
hacs = await async_load_from_store(self.hass, "hacs")
|
||||
except KeyError:
|
||||
await async_save_to_store(self.hass, "hacs", self.data.read("hacs")["data"])
|
||||
hacs = await async_load_from_store(self.hass, "hacs")
|
||||
|
||||
try:
|
||||
repositories = await async_load_from_store(self.hass, "repositories")
|
||||
except KeyError:
|
||||
await async_save_to_store(
|
||||
self.hass, "repositories", self.data.read("repositories")["data"]
|
||||
)
|
||||
repositories = await async_load_from_store(self.hass, "repositories")
|
||||
|
||||
try:
|
||||
if self.check_corrupted_files():
|
||||
# Coruptted installation
|
||||
self.logger.critical("Restore failed one or more files are corrupted!")
|
||||
return False
|
||||
if hacs is None and repositories is None:
|
||||
# Assume new install
|
||||
return True
|
||||
|
||||
self.logger.info("Restore started")
|
||||
|
||||
# Hacs
|
||||
self.configuration.frontend_mode = hacs.get("view", "Grid")
|
||||
|
||||
# Repositories
|
||||
repositories = repositories
|
||||
for entry in repositories:
|
||||
repo = repositories[entry]
|
||||
if repo["full_name"] == "custom-components/hacs":
|
||||
# Skip the old repo location
|
||||
continue
|
||||
if not self.is_known(repo["full_name"]):
|
||||
await self.register_repository(
|
||||
repo["full_name"], repo["category"], False
|
||||
)
|
||||
repository = self.get_by_name(repo["full_name"])
|
||||
if repository is None:
|
||||
self.logger.error(f"Did not find {repo['full_name']}")
|
||||
continue
|
||||
|
||||
# Restore repository attributes
|
||||
if repo.get("authors") is not None:
|
||||
repository.information.authors = repo["authors"]
|
||||
|
||||
if repo.get("topics", []):
|
||||
repository.information.topics = repo["topics"]
|
||||
|
||||
if repo.get("description") is not None:
|
||||
repository.information.description = repo["description"]
|
||||
|
||||
if repo.get("name") is not None:
|
||||
repository.information.name = repo["name"]
|
||||
|
||||
if repo.get("hide") is not None:
|
||||
repository.status.hide = repo["hide"]
|
||||
|
||||
if repo.get("installed") is not None:
|
||||
repository.status.installed = repo["installed"]
|
||||
if repository.status.installed:
|
||||
repository.status.first_install = False
|
||||
|
||||
if repo.get("selected_tag") is not None:
|
||||
repository.status.selected_tag = repo["selected_tag"]
|
||||
|
||||
if repo.get("repository_manifest") is not None:
|
||||
repository.repository_manifest = HacsManifest.from_dict(
|
||||
repo["repository_manifest"]
|
||||
)
|
||||
|
||||
if repo.get("show_beta") is not None:
|
||||
repository.status.show_beta = repo["show_beta"]
|
||||
|
||||
if repo.get("last_commit") is not None:
|
||||
repository.versions.available_commit = repo["last_commit"]
|
||||
|
||||
repository.information.uid = entry
|
||||
|
||||
if repo.get("last_release_tag") is not None:
|
||||
repository.releases.last_release = repo["last_release_tag"]
|
||||
repository.versions.available = repo["last_release_tag"]
|
||||
|
||||
if repo.get("new") is not None:
|
||||
repository.status.new = repo["new"]
|
||||
|
||||
if repo["full_name"] == "hacs/integration":
|
||||
repository.versions.installed = VERSION
|
||||
repository.status.installed = True
|
||||
if "b" in VERSION:
|
||||
repository.status.show_beta = True
|
||||
elif repo.get("version_installed") is not None:
|
||||
repository.versions.installed = repo["version_installed"]
|
||||
|
||||
if repo.get("installed_commit") is not None:
|
||||
repository.versions.installed_commit = repo["installed_commit"]
|
||||
|
||||
self.logger.info("Restore done")
|
||||
except Exception as exception:
|
||||
self.logger.critical(
|
||||
f"[{exception}] Restore Failed! see https://github.com/hacs/integration/issues/639 for more details."
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def save(logger, path, content):
|
||||
"""Save file."""
|
||||
from .backup import Backup
|
||||
|
||||
backup = Backup(path)
|
||||
backup.create()
|
||||
try:
|
||||
content = {"data": content, "schema": STORAGE_VERSION}
|
||||
with open(path, "w", encoding="utf-8") as storefile:
|
||||
json.dump(content, storefile, indent=4)
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
logger.warning(f"Saving {path} failed - {exception}")
|
||||
backup.restore()
|
||||
backup.cleanup()
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Custom Exceptions."""
|
||||
|
||||
|
||||
class HacsBaseException(Exception):
|
||||
"""Super basic."""
|
||||
|
||||
|
||||
class HacsUserScrewupException(HacsBaseException):
|
||||
"""Raise this when the user does something they should not do."""
|
||||
|
||||
|
||||
class HacsNotSoBasicException(HacsBaseException):
|
||||
"""Not that basic."""
|
||||
|
||||
|
||||
class HacsDataFileMissing(HacsBaseException):
|
||||
"""Raise this storage datafile is missing."""
|
||||
|
||||
|
||||
class HacsDataNotExpected(HacsBaseException):
|
||||
"""Raise this when data returned from storage is not ok."""
|
||||
|
||||
|
||||
class HacsRepositoryInfo(HacsBaseException):
|
||||
"""Raise this when repository info is missing/wrong."""
|
||||
|
||||
|
||||
class HacsRequirement(HacsBaseException):
|
||||
"""Raise this when repository is missing a requirement."""
|
||||
|
||||
|
||||
class HacsMissingManifest(HacsBaseException):
|
||||
"""Raise this when manifest is missing."""
|
||||
|
||||
def __init__(self, message="The manifest file is missing in the repository."):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class HacsBlacklistException(HacsBaseException):
|
||||
"""Raise this when the repository is currently in the blacklist."""
|
||||
|
||||
def __init__(self, message="The repository is currently in the blacklist."):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
@@ -1,129 +0,0 @@
|
||||
"""HACS Migration logic."""
|
||||
# pylint: disable=broad-except,no-member
|
||||
from shutil import copy2
|
||||
|
||||
from integrationhelper import Logger, Validate
|
||||
|
||||
from . import Hacs
|
||||
from .data import save, STORES
|
||||
from .const import STORAGE_VERSION
|
||||
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
|
||||
def register(cls):
|
||||
"""Register steps."""
|
||||
MIGRATIONS[cls.from_version] = cls
|
||||
return cls
|
||||
|
||||
|
||||
class ValidateData(Hacs):
|
||||
"""Validate."""
|
||||
|
||||
def validate_local_data_file(self):
|
||||
"""Validate content."""
|
||||
validate = Validate()
|
||||
old_data = self.data.read("old")
|
||||
|
||||
if old_data is None:
|
||||
# new install.
|
||||
return True
|
||||
|
||||
if old_data.get("hacs", {}).get("schema") is None:
|
||||
return True
|
||||
|
||||
if old_data.get("hacs", {}).get("schema") == STORAGE_VERSION:
|
||||
# Newest version, no need to do anything.
|
||||
return
|
||||
|
||||
current = old_data.get("hacs", {}).get("schema")
|
||||
|
||||
for version in range(int(current), int(STORAGE_VERSION)):
|
||||
if current in MIGRATIONS:
|
||||
MIGRATIONS[current](old_data).migrate()
|
||||
else:
|
||||
validate.errors.append(f"Missing migration step for {version}")
|
||||
|
||||
if validate.errors:
|
||||
for error in validate.errors:
|
||||
self.logger.critical(error)
|
||||
return validate.success
|
||||
|
||||
|
||||
class Migration(Hacs):
|
||||
"""Hacs migrations"""
|
||||
|
||||
def __init__(self, old_data=None):
|
||||
"""initialize migration."""
|
||||
self.old_data = old_data
|
||||
self.logger = Logger("hacs.migration")
|
||||
self.source = f"{self.system.config_path}/.storage/hacs"
|
||||
self.cleanup()
|
||||
self.backup()
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove files no longer in use."""
|
||||
|
||||
def backup(self):
|
||||
"""Back up old file."""
|
||||
destination = f"{self.source}.{self.from_version}"
|
||||
self.logger.info(f"Backing up current file to '{destination}'")
|
||||
copy2(self.source, destination)
|
||||
|
||||
|
||||
@register
|
||||
class FromVersion4(Migration):
|
||||
"""Migrate from version 4"""
|
||||
|
||||
from_version = "4"
|
||||
|
||||
def migrate(self):
|
||||
"""Start migration."""
|
||||
self.logger.info(f"Starting migration from {self.from_version}")
|
||||
hacs = self.old_data["hacs"]
|
||||
repositories = self.old_data["repositories"]
|
||||
installed = {}
|
||||
|
||||
for repository in repositories:
|
||||
repository = repositories[repository]
|
||||
repository["full_name"] = repository["repository_name"]
|
||||
repository["category"] = repository["repository_type"]
|
||||
if not repository["installed"]:
|
||||
repository["show_beta"] = False
|
||||
else:
|
||||
if repository["version_installed"] is not None:
|
||||
version_type = "version"
|
||||
version_installed = repository["version_installed"]
|
||||
version_available = repository["last_release_tag"]
|
||||
else:
|
||||
version_type = "commit"
|
||||
version_installed = repository["installed_commit"]
|
||||
version_available = repository["last_commit"]
|
||||
if repository["full_name"] != "hacs/integration":
|
||||
installed[repository["repository_name"]] = {
|
||||
"version_type": str(version_type),
|
||||
"version_installed": str(version_installed),
|
||||
"version_available": str(version_available),
|
||||
}
|
||||
|
||||
path = f"{self.system.config_path}/.storage/{STORES['hacs']}"
|
||||
save(self.logger, path, hacs)
|
||||
|
||||
path = f"{self.system.config_path}/.storage/{STORES['repositories']}"
|
||||
save(self.logger, path, repositories)
|
||||
|
||||
path = f"{self.system.config_path}/.storage/{STORES['installed']}"
|
||||
save(self.logger, path, installed)
|
||||
self.logger.info("Migration done")
|
||||
|
||||
|
||||
@register
|
||||
class FromVersion5(Migration):
|
||||
"""Migrate from version 5"""
|
||||
|
||||
from_version = "5"
|
||||
|
||||
def migrate(self):
|
||||
"""Start migration."""
|
||||
self.logger.info("Migration done")
|
||||
@@ -1,16 +0,0 @@
|
||||
"""HACS Startup."""
|
||||
from . import Hacs
|
||||
|
||||
|
||||
class HacsStartup(Hacs):
|
||||
"""Startup class."""
|
||||
|
||||
async def run_startup(self):
|
||||
"""Run startup tasks for HACS."""
|
||||
self.logger.critical("Startup!")
|
||||
if self.configuration.dev:
|
||||
self.logger.critical("Running in DEV mode!")
|
||||
if self.developer.devcontainer:
|
||||
self.logger.critical("Running inside a devcontainer")
|
||||
self.logger.critical("Some features have been disabled")
|
||||
self.logger.critical("It will only fetch one repository pr category")
|
||||
@@ -1,6 +0,0 @@
|
||||
"""HACS Updater."""
|
||||
from . import Hacs
|
||||
|
||||
|
||||
class HacsUpdate(Hacs):
|
||||
"""Update class."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Initialize handlers."""
|
||||
@@ -1,78 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,29 +0,0 @@
|
||||
"""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
|
||||
@@ -1,27 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,262 +0,0 @@
|
||||
"""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",
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Initialize repositories."""
|
||||
from .theme import HacsTheme
|
||||
from .integration import HacsIntegration
|
||||
from .python_script import HacsPythonScript
|
||||
from .appdaemon import HacsAppdaemon
|
||||
from .plugin import HacsPlugin
|
||||
@@ -1,87 +0,0 @@
|
||||
"""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
|
||||
@@ -1,164 +0,0 @@
|
||||
"""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
|
||||
@@ -1,41 +0,0 @@
|
||||
"""
|
||||
Manifest handling of a repository.
|
||||
|
||||
https://hacs.xyz/docs/publish/start#hacsjson
|
||||
"""
|
||||
from typing import List
|
||||
import attr
|
||||
|
||||
from custom_components.hacs.hacsbase.exceptions import HacsRepositoryInfo
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class HacsManifest:
|
||||
"""HacsManifest class."""
|
||||
|
||||
name: str = None
|
||||
content_in_root: bool = False
|
||||
zip_release: bool = False
|
||||
filename: str = None
|
||||
manifest: dict = {}
|
||||
hacs: str = None
|
||||
domains: List[str] = []
|
||||
country: List[str] = []
|
||||
homeassistant: str = None
|
||||
persistent_directory: str = None
|
||||
iot_class: str = None
|
||||
render_readme: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_dict(manifest: dict):
|
||||
"""Set attributes from dicts."""
|
||||
if manifest is None:
|
||||
raise HacsRepositoryInfo("Missing manifest data")
|
||||
|
||||
manifest_data = HacsManifest()
|
||||
|
||||
manifest_data.manifest = manifest
|
||||
|
||||
for key in manifest:
|
||||
setattr(manifest_data, key, manifest[key])
|
||||
return manifest_data
|
||||
@@ -1,169 +0,0 @@
|
||||
"""Class for plugins in HACS."""
|
||||
import json
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from .repository import HacsRepository, register_repository_class
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsPlugin(HacsRepository):
|
||||
"""Plugins in HACS."""
|
||||
|
||||
category = "plugin"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.information.file_name = None
|
||||
self.information.javascript_type = None
|
||||
self.content.path.local = (
|
||||
f"{self.system.config_path}/www/community/{full_name.split('/')[-1]}"
|
||||
)
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
# Run common validation steps.
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
await self.get_plugin_location()
|
||||
|
||||
if self.content.path.remote is None:
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
if self.content.path.remote == "release":
|
||||
self.content.single = True
|
||||
|
||||
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()
|
||||
|
||||
async def update_repository(self):
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
return
|
||||
# Run common update steps.
|
||||
await self.common_update()
|
||||
|
||||
# Get plugin objects.
|
||||
await self.get_plugin_location()
|
||||
|
||||
# Get JS type
|
||||
await self.parse_readme_for_jstype()
|
||||
|
||||
if self.content.path.remote is None:
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
if self.content.path.remote == "release":
|
||||
self.content.single = True
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
|
||||
async def get_plugin_location(self):
|
||||
"""Get plugin location."""
|
||||
if self.content.path.remote is not None:
|
||||
return
|
||||
|
||||
possible_locations = ["dist", "release", ""]
|
||||
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
possible_locations = [""]
|
||||
|
||||
for location in possible_locations:
|
||||
if self.content.path.remote is not None:
|
||||
continue
|
||||
try:
|
||||
objects = []
|
||||
files = []
|
||||
if location != "release":
|
||||
try:
|
||||
objects = await self.repository_object.get_contents(
|
||||
location, self.ref
|
||||
)
|
||||
except AIOGitHubException:
|
||||
continue
|
||||
else:
|
||||
await self.get_releases()
|
||||
if self.releases.releases:
|
||||
if self.releases.last_release_object.assets is not None:
|
||||
objects = self.releases.last_release_object.assets
|
||||
|
||||
for item in objects:
|
||||
if item.name.endswith(".js"):
|
||||
files.append(item.name)
|
||||
|
||||
# Handler for plug requirement 3
|
||||
valid_filenames = [
|
||||
f"{self.information.name.replace('lovelace-', '')}.js",
|
||||
f"{self.information.name}.js",
|
||||
f"{self.information.name}.umd.js",
|
||||
f"{self.information.name}-bundle.js",
|
||||
]
|
||||
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.filename:
|
||||
valid_filenames.append(self.repository_manifest.filename)
|
||||
|
||||
for name in valid_filenames:
|
||||
if name in files:
|
||||
# YES! We got it!
|
||||
self.information.file_name = name
|
||||
self.content.path.remote = location
|
||||
self.content.objects = objects
|
||||
self.content.files = files
|
||||
break
|
||||
|
||||
except SystemError:
|
||||
pass
|
||||
|
||||
async def get_package_content(self):
|
||||
"""Get package content."""
|
||||
try:
|
||||
package = await self.repository_object.get_contents("package.json")
|
||||
package = json.loads(package.content)
|
||||
|
||||
if package:
|
||||
self.information.authors = package["author"]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
async def parse_readme_for_jstype(self):
|
||||
"""Parse the readme looking for js type."""
|
||||
readme = None
|
||||
readme_files = ["readme", "readme.md"]
|
||||
root = await self.repository_object.get_contents("")
|
||||
for file in root:
|
||||
if file.name.lower() in readme_files:
|
||||
readme = await self.repository_object.get_contents(file.name)
|
||||
break
|
||||
|
||||
if readme is None:
|
||||
return
|
||||
|
||||
readme = readme.content
|
||||
for line in readme.splitlines():
|
||||
if "type: module" in line:
|
||||
self.information.javascript_type = "module"
|
||||
break
|
||||
elif "type: js" in line:
|
||||
self.information.javascript_type = "js"
|
||||
break
|
||||
@@ -1,79 +0,0 @@
|
||||
"""Class for python_scripts in HACS."""
|
||||
from .repository import HacsRepository, register_repository_class
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsPythonScript(HacsRepository):
|
||||
"""python_scripts in HACS."""
|
||||
|
||||
category = "python_script"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.content.path.remote = "python_scripts"
|
||||
self.content.path.local = f"{self.system.config_path}/python_scripts"
|
||||
self.content.single = True
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
# Run common validation steps.
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
if not isinstance(self.content.objects, list):
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
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 name
|
||||
self.information.name = self.content.objects[0].name.replace(".py", "")
|
||||
|
||||
async def update_repository(self): # lgtm[py/similar-function]
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
return
|
||||
# Run common update steps.
|
||||
await self.common_update()
|
||||
|
||||
# Get python_script objects.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
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)
|
||||
|
||||
# Update name
|
||||
self.information.name = self.content.objects[0].name.replace(".py", "")
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
@@ -1,697 +0,0 @@
|
||||
"""Repository."""
|
||||
# pylint: disable=broad-except, bad-continuation, no-member
|
||||
import pathlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from integrationhelper import Validate, Logger
|
||||
from aiogithubapi import AIOGitHubException
|
||||
from .manifest import HacsManifest
|
||||
from ..helpers.misc import get_repository_name
|
||||
from ..hacsbase import Hacs
|
||||
from ..hacsbase.backup import Backup
|
||||
from ..handler.download import async_download_file, async_save_file
|
||||
from ..helpers.misc import version_is_newer_than_version
|
||||
|
||||
|
||||
RERPOSITORY_CLASSES = {}
|
||||
|
||||
|
||||
def register_repository_class(cls):
|
||||
"""Register class."""
|
||||
RERPOSITORY_CLASSES[cls.category] = cls
|
||||
return cls
|
||||
|
||||
|
||||
class RepositoryVersions:
|
||||
"""Versions."""
|
||||
|
||||
available = None
|
||||
available_commit = None
|
||||
installed = None
|
||||
installed_commit = None
|
||||
|
||||
|
||||
class RepositoryStatus:
|
||||
"""Repository status."""
|
||||
|
||||
hide = False
|
||||
installed = False
|
||||
last_updated = None
|
||||
new = True
|
||||
selected_tag = None
|
||||
show_beta = False
|
||||
track = True
|
||||
updated_info = False
|
||||
first_install = True
|
||||
|
||||
|
||||
class RepositoryInformation:
|
||||
"""RepositoryInformation."""
|
||||
|
||||
additional_info = None
|
||||
authors = []
|
||||
category = None
|
||||
default_branch = None
|
||||
description = ""
|
||||
state = None
|
||||
full_name = None
|
||||
file_name = None
|
||||
javascript_type = None
|
||||
homeassistant_version = None
|
||||
last_updated = None
|
||||
uid = None
|
||||
info = None
|
||||
name = None
|
||||
topics = []
|
||||
|
||||
|
||||
class RepositoryReleases:
|
||||
"""RepositoyReleases."""
|
||||
|
||||
last_release = None
|
||||
last_release_object = None
|
||||
published_tags = []
|
||||
objects = []
|
||||
releases = False
|
||||
|
||||
|
||||
class RepositoryPath:
|
||||
"""RepositoryPath."""
|
||||
|
||||
local = None
|
||||
remote = None
|
||||
|
||||
|
||||
class RepositoryContent:
|
||||
"""RepositoryContent."""
|
||||
|
||||
path = None
|
||||
files = []
|
||||
objects = []
|
||||
single = False
|
||||
|
||||
|
||||
class HacsRepository(Hacs):
|
||||
"""HacsRepository."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set up HacsRepository."""
|
||||
|
||||
self.content = RepositoryContent()
|
||||
self.content.path = RepositoryPath()
|
||||
self.information = RepositoryInformation()
|
||||
self.repository_object = None
|
||||
self.status = RepositoryStatus()
|
||||
self.state = None
|
||||
self.manifest = {}
|
||||
self.repository_manifest = HacsManifest.from_dict({})
|
||||
self.validate = Validate()
|
||||
self.releases = RepositoryReleases()
|
||||
self.versions = RepositoryVersions()
|
||||
self.pending_restart = False
|
||||
self.logger = None
|
||||
|
||||
@property
|
||||
def pending_upgrade(self):
|
||||
"""Return pending upgrade."""
|
||||
if self.status.installed:
|
||||
if self.display_installed_version != self.display_available_version:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def ref(self):
|
||||
"""Return the ref."""
|
||||
if self.status.selected_tag is not None:
|
||||
if self.status.selected_tag == self.information.default_branch:
|
||||
return self.information.default_branch
|
||||
return "tags/{}".format(self.status.selected_tag)
|
||||
|
||||
if self.releases.releases:
|
||||
return "tags/{}".format(self.versions.available)
|
||||
|
||||
return self.information.default_branch
|
||||
|
||||
@property
|
||||
def custom(self):
|
||||
"""Return flag if the repository is custom."""
|
||||
if self.information.full_name.split("/")[0] in [
|
||||
"custom-components",
|
||||
"custom-cards",
|
||||
]:
|
||||
return False
|
||||
if self.information.full_name in self.common.default:
|
||||
return False
|
||||
if self.information.full_name == "hacs/integration":
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def can_install(self):
|
||||
"""Return bool if repository can be installed."""
|
||||
target = None
|
||||
if self.information.homeassistant_version is not None:
|
||||
target = self.information.homeassistant_version
|
||||
if self.repository_manifest is not None:
|
||||
if self.repository_manifest.homeassistant is not None:
|
||||
target = self.repository_manifest.homeassistant
|
||||
|
||||
if target is not None:
|
||||
if self.releases.releases:
|
||||
if version_is_newer_than_version(target, self.system.ha_version):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Return display name."""
|
||||
return get_repository_name(
|
||||
self.repository_manifest,
|
||||
self.information.name,
|
||||
self.information.category,
|
||||
self.manifest,
|
||||
)
|
||||
|
||||
@property
|
||||
def display_status(self):
|
||||
"""Return display_status."""
|
||||
if self.status.new:
|
||||
status = "new"
|
||||
elif self.pending_restart:
|
||||
status = "pending-restart"
|
||||
elif self.pending_upgrade:
|
||||
status = "pending-upgrade"
|
||||
elif self.status.installed:
|
||||
status = "installed"
|
||||
else:
|
||||
status = "default"
|
||||
return status
|
||||
|
||||
@property
|
||||
def display_status_description(self):
|
||||
"""Return display_status_description."""
|
||||
description = {
|
||||
"default": "Not installed.",
|
||||
"pending-restart": "Restart pending.",
|
||||
"pending-upgrade": "Upgrade pending.",
|
||||
"installed": "No action required.",
|
||||
"new": "This is a newly added repository.",
|
||||
}
|
||||
return description[self.display_status]
|
||||
|
||||
@property
|
||||
def display_installed_version(self):
|
||||
"""Return display_authors"""
|
||||
if self.versions.installed is not None:
|
||||
installed = self.versions.installed
|
||||
else:
|
||||
if self.versions.installed_commit is not None:
|
||||
installed = self.versions.installed_commit
|
||||
else:
|
||||
installed = ""
|
||||
return installed
|
||||
|
||||
@property
|
||||
def display_available_version(self):
|
||||
"""Return display_authors"""
|
||||
if self.versions.available is not None:
|
||||
available = self.versions.available
|
||||
else:
|
||||
if self.versions.available_commit is not None:
|
||||
available = self.versions.available_commit
|
||||
else:
|
||||
available = ""
|
||||
return available
|
||||
|
||||
@property
|
||||
def display_version_or_commit(self):
|
||||
"""Does the repositoriy use releases or commits?"""
|
||||
if self.versions.installed is not None:
|
||||
version_or_commit = "version"
|
||||
else:
|
||||
version_or_commit = "commit"
|
||||
return version_or_commit
|
||||
|
||||
@property
|
||||
def main_action(self):
|
||||
"""Return the main action."""
|
||||
actions = {
|
||||
"new": "INSTALL",
|
||||
"default": "INSTALL",
|
||||
"installed": "REINSTALL",
|
||||
"pending-restart": "REINSTALL",
|
||||
"pending-upgrade": "UPGRADE",
|
||||
}
|
||||
return actions[self.display_status]
|
||||
|
||||
async def common_validate(self):
|
||||
"""Common validation steps of the repository."""
|
||||
# Attach helpers
|
||||
self.validate.errors = []
|
||||
self.logger = Logger(
|
||||
f"hacs.repository.{self.information.category}.{self.information.full_name}"
|
||||
)
|
||||
|
||||
# Step 1: Make sure the repository exist.
|
||||
self.logger.debug("Checking repository.")
|
||||
try:
|
||||
self.repository_object = await self.github.get_repo(
|
||||
self.information.full_name
|
||||
)
|
||||
except Exception as exception: # Gotta Catch 'Em All
|
||||
if not self.system.status.startup:
|
||||
self.logger.error(exception)
|
||||
self.validate.errors.append("Repository does not exist.")
|
||||
return
|
||||
|
||||
# Step 2: Make sure the repository is not archived.
|
||||
if self.repository_object.archived:
|
||||
self.validate.errors.append("Repository is archived.")
|
||||
return
|
||||
|
||||
# Step 3: Make sure the repository is not in the blacklist.
|
||||
if self.information.full_name in self.common.blacklist:
|
||||
self.validate.errors.append("Repository is in the blacklist.")
|
||||
return
|
||||
|
||||
# Step 4: default branch
|
||||
self.information.default_branch = self.repository_object.default_branch
|
||||
|
||||
# Step 5: Get releases.
|
||||
await self.get_releases()
|
||||
|
||||
# Step 6: Get the content of hacs.json
|
||||
await self.get_repository_manifest_content()
|
||||
|
||||
# Set repository name
|
||||
self.information.name = self.information.full_name.split("/")[1]
|
||||
|
||||
async def common_registration(self):
|
||||
"""Common registration steps of the repository."""
|
||||
# Attach logger
|
||||
if self.logger is None:
|
||||
self.logger = Logger(
|
||||
f"hacs.repository.{self.information.category}.{self.information.full_name}"
|
||||
)
|
||||
|
||||
# Attach repository
|
||||
if self.repository_object is None:
|
||||
self.repository_object = await self.github.get_repo(
|
||||
self.information.full_name
|
||||
)
|
||||
|
||||
# Set id
|
||||
self.information.uid = str(self.repository_object.id)
|
||||
|
||||
# Set topics
|
||||
self.information.topics = self.repository_object.topics
|
||||
|
||||
# Set description
|
||||
if self.repository_object.description:
|
||||
self.information.description = self.repository_object.description
|
||||
|
||||
async def common_update(self):
|
||||
"""Common information update steps of the repository."""
|
||||
# Attach logger
|
||||
if self.logger is None:
|
||||
self.logger = Logger(
|
||||
f"hacs.repository.{self.information.category}.{self.information.full_name}"
|
||||
)
|
||||
|
||||
# Attach repository
|
||||
self.repository_object = await self.github.get_repo(self.information.full_name)
|
||||
|
||||
# Update description
|
||||
if self.repository_object.description:
|
||||
self.information.description = self.repository_object.description
|
||||
|
||||
# Update default branch
|
||||
self.information.default_branch = self.repository_object.default_branch
|
||||
|
||||
# Update last available commit
|
||||
await self.repository_object.set_last_commit()
|
||||
self.versions.available_commit = self.repository_object.last_commit
|
||||
|
||||
# Update last updaeted
|
||||
self.information.last_updated = self.repository_object.pushed_at
|
||||
|
||||
# Update topics
|
||||
self.information.topics = self.repository_object.topics
|
||||
|
||||
# Get the content of hacs.json
|
||||
await self.get_repository_manifest_content()
|
||||
|
||||
# Update "info.md"
|
||||
await self.get_info_md_content()
|
||||
|
||||
# Update releases
|
||||
await self.get_releases()
|
||||
|
||||
async def install(self):
|
||||
"""Common installation steps of the repository."""
|
||||
self.validate.errors = []
|
||||
persistent_directory = None
|
||||
|
||||
await self.update_repository()
|
||||
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.persistent_directory:
|
||||
if os.path.exists(
|
||||
f"{self.content.path.local}/{self.repository_manifest.persistent_directory}"
|
||||
):
|
||||
persistent_directory = Backup(
|
||||
f"{self.content.path.local}/{self.repository_manifest.persistent_directory}",
|
||||
tempfile.TemporaryFile() + "/hacs_persistent_directory/",
|
||||
)
|
||||
persistent_directory.create()
|
||||
|
||||
if self.status.installed and not self.content.single:
|
||||
backup = Backup(self.content.path.local)
|
||||
backup.create()
|
||||
|
||||
if self.repository_manifest.zip_release:
|
||||
validate = await self.download_zip(self.validate)
|
||||
else:
|
||||
validate = await self.download_content(
|
||||
self.validate,
|
||||
self.content.path.remote,
|
||||
self.content.path.local,
|
||||
self.ref,
|
||||
)
|
||||
|
||||
if validate.errors:
|
||||
for error in validate.errors:
|
||||
self.logger.error(error)
|
||||
if self.status.installed and not self.content.single:
|
||||
backup.restore()
|
||||
|
||||
if self.status.installed and not self.content.single:
|
||||
backup.cleanup()
|
||||
|
||||
if persistent_directory is not None:
|
||||
persistent_directory.restore()
|
||||
persistent_directory.cleanup()
|
||||
|
||||
if validate.success:
|
||||
if self.information.full_name not in self.common.installed:
|
||||
if self.information.full_name == "hacs/integration":
|
||||
self.common.installed.append(self.information.full_name)
|
||||
self.status.installed = True
|
||||
self.versions.installed_commit = self.versions.available_commit
|
||||
|
||||
if self.status.selected_tag is not None:
|
||||
self.versions.installed = self.status.selected_tag
|
||||
else:
|
||||
self.versions.installed = self.versions.available
|
||||
|
||||
if self.information.category == "integration":
|
||||
if (
|
||||
self.config_flow
|
||||
and self.information.full_name != "hacs/integration"
|
||||
):
|
||||
await self.reload_custom_components()
|
||||
else:
|
||||
self.pending_restart = True
|
||||
|
||||
elif self.information.category == "theme":
|
||||
try:
|
||||
await self.hass.services.async_call("frontend", "reload_themes", {})
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
self.hass.bus.async_fire(
|
||||
"hacs/repository",
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "install",
|
||||
"repository": self.information.full_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def download_zip(self, validate):
|
||||
"""Download ZIP archive from repository release."""
|
||||
try:
|
||||
contents = False
|
||||
|
||||
for release in self.releases.objects:
|
||||
self.logger.info(f"ref: {self.ref} --- tag: {release.tag_name}")
|
||||
if release.tag_name == self.ref.split("/")[1]:
|
||||
contents = release.assets
|
||||
|
||||
if not contents:
|
||||
return validate
|
||||
|
||||
for content in contents or []:
|
||||
filecontent = await async_download_file(self.hass, content.download_url)
|
||||
|
||||
if filecontent is None:
|
||||
validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
continue
|
||||
|
||||
result = await async_save_file(
|
||||
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}",
|
||||
filecontent,
|
||||
)
|
||||
with zipfile.ZipFile(
|
||||
f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", "r"
|
||||
) as zip_file:
|
||||
zip_file.extractall(self.content.path.local)
|
||||
|
||||
if result:
|
||||
self.logger.info(f"download of {content.name} complete")
|
||||
continue
|
||||
validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
except Exception:
|
||||
validate.errors.append(f"Download was not complete.")
|
||||
|
||||
return validate
|
||||
|
||||
async def download_content(self, validate, directory_path, local_directory, ref):
|
||||
"""Download the content of a directory."""
|
||||
try:
|
||||
# Get content
|
||||
if self.content.single:
|
||||
contents = self.content.objects
|
||||
else:
|
||||
contents = await self.repository_object.get_contents(
|
||||
directory_path, self.ref
|
||||
)
|
||||
|
||||
for content in contents:
|
||||
if content.type == "dir" and (
|
||||
self.repository_manifest.content_in_root
|
||||
or self.content.path.remote != ""
|
||||
):
|
||||
await self.download_content(
|
||||
validate, content.path, local_directory, ref
|
||||
)
|
||||
continue
|
||||
if self.information.category == "plugin":
|
||||
if not content.name.endswith(".js"):
|
||||
if self.content.path.remote != "dist":
|
||||
continue
|
||||
|
||||
self.logger.debug(f"Downloading {content.name}")
|
||||
|
||||
filecontent = await async_download_file(self.hass, content.download_url)
|
||||
|
||||
if filecontent is None:
|
||||
validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
continue
|
||||
|
||||
# Save the content of the file.
|
||||
if self.content.single:
|
||||
local_directory = self.content.path.local
|
||||
|
||||
else:
|
||||
_content_path = content.path
|
||||
if not self.repository_manifest.content_in_root:
|
||||
_content_path = _content_path.replace(
|
||||
f"{self.content.path.remote}/", ""
|
||||
)
|
||||
|
||||
local_directory = f"{self.content.path.local}/{_content_path}"
|
||||
local_directory = local_directory.split("/")
|
||||
del local_directory[-1]
|
||||
local_directory = "/".join(local_directory)
|
||||
|
||||
# Check local directory
|
||||
pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
local_file_path = f"{local_directory}/{content.name}"
|
||||
result = await async_save_file(local_file_path, filecontent)
|
||||
if result:
|
||||
self.logger.info(f"download of {content.name} complete")
|
||||
continue
|
||||
validate.errors.append(f"[{content.name}] was not downloaded.")
|
||||
|
||||
except Exception:
|
||||
validate.errors.append(f"Download was not complete.")
|
||||
return validate
|
||||
|
||||
async def get_repository_manifest_content(self):
|
||||
"""Get the content of the hacs.json file."""
|
||||
try:
|
||||
manifest = await self.repository_object.get_contents("hacs.json", self.ref)
|
||||
self.repository_manifest = HacsManifest.from_dict(
|
||||
json.loads(manifest.content)
|
||||
)
|
||||
except (AIOGitHubException, Exception): # Gotta Catch 'Em All
|
||||
pass
|
||||
|
||||
async def get_info_md_content(self):
|
||||
"""Get the content of info.md"""
|
||||
from ..handler.template import render_template
|
||||
|
||||
info = None
|
||||
info_files = ["info", "info.md"]
|
||||
|
||||
if self.repository_manifest is not None:
|
||||
if self.repository_manifest.render_readme:
|
||||
info_files = ["readme", "readme.md"]
|
||||
try:
|
||||
root = await self.repository_object.get_contents("", self.ref)
|
||||
for file in root:
|
||||
if file.name.lower() in info_files:
|
||||
|
||||
info = await self.repository_object.get_rendered_contents(
|
||||
file.name, self.ref
|
||||
)
|
||||
break
|
||||
if info is None:
|
||||
self.information.additional_info = ""
|
||||
else:
|
||||
info = info.replace("<", "<")
|
||||
info = info.replace("<svg", "<disabled").replace("</svg", "</disabled")
|
||||
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("<li>", "<li style='list-style-type: initial;'>")
|
||||
|
||||
# Special changes that needs to be done:
|
||||
info = info.replace(
|
||||
"<your", "<‍your"
|
||||
) # for thomasloven/hass-favicon
|
||||
|
||||
info += "</br>"
|
||||
|
||||
self.information.additional_info = render_template(info, self)
|
||||
|
||||
except (AIOGitHubException, Exception):
|
||||
self.information.additional_info = ""
|
||||
|
||||
async def get_releases(self):
|
||||
"""Get repository releases."""
|
||||
if self.status.show_beta:
|
||||
self.releases.objects = await self.repository_object.get_releases(
|
||||
prerelease=True, returnlimit=self.configuration.release_limit
|
||||
)
|
||||
else:
|
||||
self.releases.objects = await self.repository_object.get_releases(
|
||||
prerelease=False, returnlimit=self.configuration.release_limit
|
||||
)
|
||||
|
||||
if not self.releases.objects:
|
||||
return
|
||||
|
||||
self.releases.releases = True
|
||||
|
||||
self.releases.published_tags = []
|
||||
|
||||
for release in self.releases.objects:
|
||||
self.releases.published_tags.append(release.tag_name)
|
||||
|
||||
self.releases.last_release_object = self.releases.objects[0]
|
||||
if self.status.selected_tag is not None:
|
||||
if self.status.selected_tag != self.information.default_branch:
|
||||
for release in self.releases.objects:
|
||||
if release.tag_name == self.status.selected_tag:
|
||||
self.releases.last_release_object = release
|
||||
break
|
||||
self.versions.available = self.releases.objects[0].tag_name
|
||||
|
||||
def remove(self):
|
||||
"""Run remove tasks."""
|
||||
# Attach logger
|
||||
if self.logger is None:
|
||||
self.logger = Logger(
|
||||
f"hacs.repository.{self.information.category}.{self.information.full_name}"
|
||||
)
|
||||
self.logger.info("Starting removal")
|
||||
|
||||
if self.information.uid in self.common.installed:
|
||||
self.common.installed.remove(self.information.uid)
|
||||
for repository in self.repositories:
|
||||
if repository.information.uid == self.information.uid:
|
||||
self.repositories.remove(repository)
|
||||
|
||||
async def uninstall(self):
|
||||
"""Run uninstall tasks."""
|
||||
# Attach logger
|
||||
if self.logger is None:
|
||||
self.logger = Logger(
|
||||
f"hacs.repository.{self.information.category}.{self.information.full_name}"
|
||||
)
|
||||
self.logger.info("Uninstalling")
|
||||
await self.remove_local_directory()
|
||||
self.status.installed = False
|
||||
if self.information.category == "integration":
|
||||
if self.config_flow:
|
||||
await self.reload_custom_components()
|
||||
else:
|
||||
self.pending_restart = True
|
||||
elif self.information.category == "theme":
|
||||
try:
|
||||
await self.hass.services.async_call("frontend", "reload_themes", {})
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
if self.information.full_name in self.common.installed:
|
||||
self.common.installed.remove(self.information.full_name)
|
||||
self.versions.installed = None
|
||||
self.versions.installed_commit = None
|
||||
self.hass.bus.async_fire(
|
||||
"hacs/repository",
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "uninstall",
|
||||
"repository": self.information.full_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def remove_local_directory(self):
|
||||
"""Check the local directory."""
|
||||
import shutil
|
||||
from asyncio import sleep
|
||||
|
||||
try:
|
||||
if self.information.category == "python_script":
|
||||
local_path = "{}/{}.py".format(
|
||||
self.content.path.local, self.information.name
|
||||
)
|
||||
elif self.information.category == "theme":
|
||||
local_path = "{}/{}.yaml".format(
|
||||
self.content.path.local, self.information.name
|
||||
)
|
||||
else:
|
||||
local_path = self.content.path.local
|
||||
|
||||
if os.path.exists(local_path):
|
||||
self.logger.debug(f"Removing {local_path}")
|
||||
|
||||
if self.information.category in ["python_script", "theme"]:
|
||||
os.remove(local_path)
|
||||
else:
|
||||
shutil.rmtree(local_path)
|
||||
|
||||
while os.path.exists(local_path):
|
||||
await sleep(1)
|
||||
|
||||
except Exception as exception:
|
||||
self.logger.debug(f"Removing {local_path} failed with {exception}")
|
||||
return
|
||||
@@ -1,78 +0,0 @@
|
||||
"""Class for themes in HACS."""
|
||||
from .repository import HacsRepository, register_repository_class
|
||||
|
||||
|
||||
@register_repository_class
|
||||
class HacsTheme(HacsRepository):
|
||||
"""Themes in HACS."""
|
||||
|
||||
category = "theme"
|
||||
|
||||
def __init__(self, full_name):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self.information.full_name = full_name
|
||||
self.information.category = self.category
|
||||
self.content.path.remote = "themes"
|
||||
self.content.path.local = f"{self.system.config_path}/themes"
|
||||
self.content.single = True
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
# Run common validation steps.
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
if not isinstance(self.content.objects, list):
|
||||
self.validate.errors.append("Repostitory structure not compliant")
|
||||
|
||||
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 name
|
||||
self.information.name = self.content.objects[0].name.replace(".yaml", "")
|
||||
|
||||
async def update_repository(self): # lgtm[py/similar-function]
|
||||
"""Update."""
|
||||
if self.github.ratelimits.remaining == 0:
|
||||
return
|
||||
# Run common update steps.
|
||||
await self.common_update()
|
||||
|
||||
# Get theme objects.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
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)
|
||||
|
||||
# Update name
|
||||
self.information.name = self.content.objects[0].name.replace(".yaml", "")
|
||||
|
||||
self.content.files = []
|
||||
for filename in self.content.objects:
|
||||
self.content.files.append(filename.name)
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Sensor platform for HACS."""
|
||||
# pylint: disable=unused-argument
|
||||
from integrationhelper import Logger
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from .hacsbase import Hacs as hacs
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Setup sensor platform."""
|
||||
async_add_entities([HACSSensor()])
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Setup sensor platform."""
|
||||
async_add_devices([HACSSensor()])
|
||||
|
||||
|
||||
class HACSSensor(Entity):
|
||||
"""HACS Sensor class."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self._state = None
|
||||
self.logger = Logger("hacs.sensor")
|
||||
self.has_update = []
|
||||
self.repositories = []
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
if hacs.system.status.background_task:
|
||||
return
|
||||
|
||||
self.repositories = []
|
||||
|
||||
for repository in hacs.repositories:
|
||||
if repository.pending_upgrade:
|
||||
self.repositories.append(repository)
|
||||
self._state = len(self.repositories)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID to use for this sensor."""
|
||||
return (
|
||||
"0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd"
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return "hacs"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the sensor."""
|
||||
return "mdi:package"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return "pending update(s)"
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return attributes for the sensor."""
|
||||
data = []
|
||||
for repository in self.repositories:
|
||||
data.append(
|
||||
{
|
||||
"name": repository.information.full_name,
|
||||
"display_name": repository.display_name,
|
||||
"installed version": repository.display_installed_version,
|
||||
"available version": repository.display_available_version,
|
||||
}
|
||||
)
|
||||
return {"repositories": data}
|
||||
@@ -1,15 +0,0 @@
|
||||
install:
|
||||
description: This is NOT intended to be used here, this is intended for developers!
|
||||
fields:
|
||||
repository:
|
||||
description: The repository ID
|
||||
example: '"123456789"'
|
||||
register:
|
||||
description: This is NOT intended to be used here, this is intended for developers!
|
||||
fields:
|
||||
repository:
|
||||
description: The full name of the repository
|
||||
example: 'developer/repo'
|
||||
repository_type:
|
||||
description: The repository type
|
||||
example: 'plugin'
|
||||
@@ -1,95 +0,0 @@
|
||||
"""Setup functions for HACS."""
|
||||
# pylint: disable=bad-continuation
|
||||
from aiogithubapi import AIOGitHubAuthentication, AIOGitHubException, AIOGitHubRatelimit
|
||||
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
from .api import HacsAPI, HacsRunningTask
|
||||
from .const import VERSION, DOMAIN
|
||||
from .http import HacsWebResponse, HacsPluginView, HacsExperimental
|
||||
from .ws_api_handlers import setup_ws_api
|
||||
|
||||
|
||||
async def load_hacs_repository(hacs):
|
||||
"""Load HACS repositroy."""
|
||||
try:
|
||||
repository = hacs().get_by_name("hacs/integration")
|
||||
if repository is None:
|
||||
await hacs().register_repository("hacs/integration", "integration")
|
||||
repository = hacs().get_by_name("hacs/integration")
|
||||
if repository is None:
|
||||
raise AIOGitHubException("Unknown error")
|
||||
repository.status.installed = True
|
||||
repository.versions.installed = VERSION
|
||||
repository.status.new = False
|
||||
hacs.repo = repository.repository_object
|
||||
hacs.data_repo = await hacs().github.get_repo("hacs/default")
|
||||
except (
|
||||
AIOGitHubException,
|
||||
AIOGitHubRatelimit,
|
||||
AIOGitHubAuthentication,
|
||||
) as exception:
|
||||
hacs.logger.critical(f"[{exception}] - Could not load HACS!")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def add_sensor(hacs):
|
||||
"""Add sensor."""
|
||||
try:
|
||||
if hacs.configuration.config_type == "yaml":
|
||||
hacs.hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hacs.hass, "sensor", DOMAIN, {}, hacs.configuration.config
|
||||
)
|
||||
)
|
||||
else:
|
||||
hacs.hass.async_add_job(
|
||||
hacs.hass.config_entries.async_forward_entry_setup(
|
||||
hacs.configuration.config_entry, "sensor"
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
async def setup_frontend(hacs):
|
||||
"""Configure the HACS frontend elements."""
|
||||
hacs.hass.http.register_view(HacsPluginView())
|
||||
|
||||
# Add to sidepanel
|
||||
if hacs.configuration.experimental:
|
||||
hacs.hass.http.register_view(HacsExperimental())
|
||||
custom_panel_config = {
|
||||
"name": "hacs-frontend",
|
||||
"embed_iframe": False,
|
||||
"trust_external": False,
|
||||
"js_url": f"/hacs_experimental/main.js?v={hacs.version}",
|
||||
}
|
||||
|
||||
config = {}
|
||||
config["_panel_custom"] = custom_panel_config
|
||||
|
||||
hacs.hass.components.frontend.async_register_built_in_panel(
|
||||
component_name="custom",
|
||||
sidebar_title=hacs.configuration.sidepanel_title,
|
||||
sidebar_icon=hacs.configuration.sidepanel_icon,
|
||||
frontend_url_path="hacs",
|
||||
config=config,
|
||||
require_admin=True,
|
||||
)
|
||||
await setup_ws_api(hacs.hass)
|
||||
else:
|
||||
# Define views
|
||||
hacs.hass.http.register_view(HacsAPI())
|
||||
# hacs.hass.http.register_view(HacsPlugin())
|
||||
hacs.hass.http.register_view(HacsRunningTask())
|
||||
hacs.hass.http.register_view(HacsWebResponse())
|
||||
hacs.hass.components.frontend.async_register_built_in_panel(
|
||||
"iframe",
|
||||
hacs.configuration.sidepanel_title,
|
||||
hacs.configuration.sidepanel_icon,
|
||||
"hacs",
|
||||
{"url": hacs.hacsweb + "/overview"},
|
||||
require_admin=True,
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Storage handers."""
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.storage import Store
|
||||
from .hacsbase.const import STORAGE_VERSION
|
||||
|
||||
|
||||
async def async_load_from_store(hass, key):
|
||||
"""Load the retained data from store and return de-serialized data."""
|
||||
store = Store(hass, STORAGE_VERSION, f"hacs.{key}", encoder=JSONEncoder)
|
||||
restored = await store.async_load()
|
||||
if restored is None:
|
||||
return {}
|
||||
return restored
|
||||
|
||||
|
||||
async def async_save_to_store(hass, key, data):
|
||||
"""Generate dynamic data to store and save it to the filesystem."""
|
||||
store = Store(hass, STORAGE_VERSION, f"hacs.{key}", encoder=JSONEncoder)
|
||||
await store.async_save(data)
|
||||
@@ -1,112 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"appdaemon": "AppDaemon",
|
||||
"appdaemon_apps": "AppDaemon Apps",
|
||||
"background_task": "Background task running, this page will reload when it's done.",
|
||||
"installed": "installed",
|
||||
"integration": "Integration",
|
||||
"integrations": "Integrations",
|
||||
"plugin": "Plugin",
|
||||
"plugins": "Plugins",
|
||||
"python_script": "Python Script",
|
||||
"python_scripts": "Python Scripts",
|
||||
"repositories": "Repositories",
|
||||
"settings": "settings",
|
||||
"theme": "Theme",
|
||||
"themes": "Themes",
|
||||
"version": "Version"
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of HACS is allowed."
|
||||
},
|
||||
"error": {
|
||||
"auth": "Personal Access Token is not correct."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"appdaemon": "Enable AppDaemon apps discovery & tracking",
|
||||
"python_script": "Enable python_scripts discovery & tracking",
|
||||
"sidepanel_icon": "Side panel icon",
|
||||
"sidepanel_title": "Side panel title",
|
||||
"theme": "Enable Themes discovery & tracking",
|
||||
"token": "GitHub Personal Access Token"
|
||||
},
|
||||
"description": "If you need help with the configuration have a look here: https:\/\/hacs.xyz\/docs\/configuration\/start",
|
||||
"title": "HACS (Home Assistant Community Store)"
|
||||
}
|
||||
},
|
||||
"title": "HACS (Home Assistant Community Store)"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "Filter with country code.",
|
||||
"experimental": "Enable experimental features",
|
||||
"release_limit": "Number of releases to show."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"delete": "Are you sure you want to delete '{item}'?",
|
||||
"uninstall": "Are you sure you want to uninstall '{item}'?",
|
||||
"exsist": "{item} already exists",
|
||||
"overwrite": "Doing this will overwrite it.",
|
||||
"continue": "Are you sure you want to continue?"
|
||||
},
|
||||
"repository": {
|
||||
"available": "Available",
|
||||
"authors": "Authors",
|
||||
"back_to": "Back to",
|
||||
"changelog": "Change log",
|
||||
"flag_this": "Flag this",
|
||||
"hide": "Hide",
|
||||
"hide_beta": "Hide beta",
|
||||
"install": "Install",
|
||||
"installed": "Installed",
|
||||
"lovelace_copy_example": "Copy the example to your clipboard",
|
||||
"lovelace_instruction": "When you add this to your lovelace configuration use this",
|
||||
"lovelace_no_js_type": "Could not determine the type of this plugin, check the repository.",
|
||||
"newest": "newest",
|
||||
"note_appdaemon": "you still need to add it to your 'apps.yaml' file",
|
||||
"note_installed": "When installed, this will be located in",
|
||||
"note_integration": "you still need to add it to your 'configuration.yaml' file",
|
||||
"note_plugin": "you still need to add it to your lovelace configuration ('ui-lovelace.yaml' or the raw UI config editor)",
|
||||
"open_issue": "Open issue",
|
||||
"open_plugin": "Open plugin",
|
||||
"reinstall": "Reinstall",
|
||||
"repository": "Repository",
|
||||
"show_beta": "Show beta",
|
||||
"uninstall": "Uninstall",
|
||||
"update_information": "Update information",
|
||||
"upgrade": "Update"
|
||||
},
|
||||
"repository_banner": {
|
||||
"not_loaded": "Not loaded",
|
||||
"restart_pending": "Restart pending",
|
||||
"integration_not_loaded": "This integration is not loaded in Home Assistant.",
|
||||
"plugin_not_loaded": "This plugin is not added to your Lovelace resources.",
|
||||
"restart": "You need to restart Home Assistant."
|
||||
},
|
||||
"settings": {
|
||||
"add_custom_repository": "ADD CUSTOM REPOSITORY",
|
||||
"category": "Category",
|
||||
"custom_repositories": "CUSTOM REPOSITORIES",
|
||||
"delete": "Delete",
|
||||
"display": "Display",
|
||||
"grid": "Grid",
|
||||
"hacs_repo": "HACS repo",
|
||||
"reload_data": "Reload data",
|
||||
"save": "Save",
|
||||
"table": "Table",
|
||||
"upgrade_all": "Upgrade all"
|
||||
},
|
||||
"store": {
|
||||
"placeholder_search": "Please enter a search term...",
|
||||
"clear_new": "Clear all new repositories",
|
||||
"new_repositories": "New Repositories"
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
"""WebSocket API for HACS."""
|
||||
# pylint: disable=unused-argument
|
||||
import os
|
||||
import voluptuous as vol
|
||||
from homeassistant.components import websocket_api
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .hacsbase import Hacs
|
||||
from .store import async_load_from_store, async_save_to_store
|
||||
|
||||
|
||||
async def setup_ws_api(hass):
|
||||
"""Set up WS API handlers."""
|
||||
websocket_api.async_register_command(hass, hacs_settings)
|
||||
websocket_api.async_register_command(hass, hacs_config)
|
||||
websocket_api.async_register_command(hass, hacs_repositories)
|
||||
websocket_api.async_register_command(hass, hacs_repository)
|
||||
websocket_api.async_register_command(hass, hacs_repository_data)
|
||||
websocket_api.async_register_command(hass, check_local_path)
|
||||
websocket_api.async_register_command(hass, hacs_status)
|
||||
websocket_api.async_register_command(hass, acknowledge_critical_repository)
|
||||
websocket_api.async_register_command(hass, get_critical_repositories)
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "hacs/settings",
|
||||
vol.Optional("action"): cv.string,
|
||||
vol.Optional("category"): cv.string,
|
||||
}
|
||||
)
|
||||
async def hacs_settings(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
action = msg["action"]
|
||||
Hacs().logger.debug(f"WS action '{action}'")
|
||||
|
||||
if action == "set_fe_grid":
|
||||
Hacs().configuration.frontend_mode = "Grid"
|
||||
|
||||
elif action == "set_fe_table":
|
||||
Hacs().configuration.frontend_mode = "Table"
|
||||
|
||||
elif action == "reload_data":
|
||||
Hacs().system.status.reloading_data = True
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
await Hacs().recuring_tasks_all()
|
||||
Hacs().system.status.reloading_data = False
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
|
||||
elif action == "upgrade_all":
|
||||
Hacs().system.status.upgrading_all = True
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
for repository in Hacs().repositories:
|
||||
if repository.pending_upgrade:
|
||||
repository.status.selected_tag = None
|
||||
await repository.install()
|
||||
Hacs().system.status.upgrading_all = False
|
||||
hass.bus.async_fire("hacs/status", {})
|
||||
hass.bus.async_fire("hacs/repository", {})
|
||||
|
||||
elif action == "clear_new":
|
||||
for repo in Hacs().repositories:
|
||||
if msg.get("category") == repo.information.category:
|
||||
if repo.status.new:
|
||||
Hacs().logger.debug(
|
||||
f"Clearing new flag from '{repo.information.full_name}'"
|
||||
)
|
||||
repo.status.new = False
|
||||
else:
|
||||
Hacs().logger.error(f"WS action '{action}' is not valid")
|
||||
await Hacs().data.async_write()
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({vol.Required("type"): "hacs/config"})
|
||||
async def hacs_config(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
config = Hacs().configuration
|
||||
|
||||
content = {}
|
||||
content["frontend_mode"] = config.frontend_mode
|
||||
content["version"] = Hacs().version
|
||||
content["dev"] = config.dev
|
||||
content["appdaemon"] = config.appdaemon
|
||||
content["python_script"] = config.python_script
|
||||
content["theme"] = config.theme
|
||||
content["country"] = config.country
|
||||
content["categories"] = Hacs().common.categories
|
||||
|
||||
connection.send_message(websocket_api.result_message(msg["id"], content))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({vol.Required("type"): "hacs/status"})
|
||||
async def hacs_status(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
content = {
|
||||
"startup": Hacs().system.status.startup,
|
||||
"background_task": Hacs().system.status.background_task,
|
||||
"reloading_data": Hacs().system.status.reloading_data,
|
||||
"upgrading_all": Hacs().system.status.upgrading_all,
|
||||
"disabled": Hacs().system.disabled,
|
||||
}
|
||||
connection.send_message(websocket_api.result_message(msg["id"], content))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({vol.Required("type"): "hacs/repositories"})
|
||||
async def hacs_repositories(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
repositories = Hacs().repositories
|
||||
content = []
|
||||
for repo in repositories:
|
||||
content.append(
|
||||
{
|
||||
"name": repo.display_name,
|
||||
"description": repo.information.description,
|
||||
"category": repo.information.category,
|
||||
"installed": repo.status.installed,
|
||||
"id": repo.information.uid,
|
||||
"can_install": repo.can_install,
|
||||
"homeassistant": repo.repository_manifest.homeassistant,
|
||||
"hide": repo.status.hide,
|
||||
"new": repo.status.new,
|
||||
"beta": repo.status.show_beta,
|
||||
"status": repo.display_status,
|
||||
"status_description": repo.display_status_description,
|
||||
"additional_info": repo.information.additional_info,
|
||||
"info": repo.information.info,
|
||||
"updated_info": repo.status.updated_info,
|
||||
"version_or_commit": repo.display_version_or_commit,
|
||||
"custom": repo.custom,
|
||||
"domain": repo.manifest.get("domain"),
|
||||
"state": repo.state,
|
||||
"installed_version": repo.display_installed_version,
|
||||
"available_version": repo.display_available_version,
|
||||
"main_action": repo.main_action,
|
||||
"pending_upgrade": repo.pending_upgrade,
|
||||
"full_name": repo.information.full_name,
|
||||
"file_name": repo.information.file_name,
|
||||
"javascript_type": repo.information.javascript_type,
|
||||
"authors": repo.information.authors,
|
||||
"local_path": repo.content.path.local,
|
||||
"topics": repo.information.topics,
|
||||
"releases": repo.releases.published_tags,
|
||||
"selected_tag": repo.status.selected_tag,
|
||||
"default_branch": repo.information.default_branch,
|
||||
}
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(msg["id"], content))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "hacs/repository",
|
||||
vol.Optional("action"): cv.string,
|
||||
vol.Optional("repository"): cv.string,
|
||||
}
|
||||
)
|
||||
async def hacs_repository(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
repo_id = msg.get("repository")
|
||||
action = msg.get("action")
|
||||
|
||||
if repo_id is None or action is None:
|
||||
return
|
||||
|
||||
repository = Hacs().get_by_id(repo_id)
|
||||
Hacs().logger.info(f"Running {action} for {repository.information.full_name}")
|
||||
|
||||
if action == "update":
|
||||
await repository.update_repository()
|
||||
repository.status.updated_info = True
|
||||
repository.status.new = False
|
||||
|
||||
elif action == "install":
|
||||
await repository.install()
|
||||
|
||||
elif action == "uninstall":
|
||||
await repository.uninstall()
|
||||
|
||||
elif action == "hide":
|
||||
repository.status.hide = True
|
||||
|
||||
elif action == "unhide":
|
||||
repository.status.hide = False
|
||||
|
||||
elif action == "show_beta":
|
||||
repository.status.show_beta = True
|
||||
await repository.update_repository()
|
||||
|
||||
elif action == "hide_beta":
|
||||
repository.status.show_beta = False
|
||||
await repository.update_repository()
|
||||
|
||||
elif action == "delete":
|
||||
repository.status.show_beta = False
|
||||
repository.remove()
|
||||
|
||||
elif action == "set_version":
|
||||
if msg["version"] == repository.information.default_branch:
|
||||
repository.status.selected_tag = None
|
||||
else:
|
||||
repository.status.selected_tag = msg["version"]
|
||||
await repository.update_repository()
|
||||
|
||||
else:
|
||||
Hacs().logger.error(f"WS action '{action}' is not valid")
|
||||
|
||||
repository.state = None
|
||||
await Hacs().data.async_write()
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "hacs/repository/data",
|
||||
vol.Optional("action"): cv.string,
|
||||
vol.Optional("repository"): cv.string,
|
||||
vol.Optional("data"): cv.string,
|
||||
}
|
||||
)
|
||||
async def hacs_repository_data(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
repo_id = msg.get("repository")
|
||||
action = msg.get("action")
|
||||
data = msg.get("data")
|
||||
|
||||
if repo_id is None:
|
||||
return
|
||||
|
||||
if action == "add":
|
||||
if "github." in repo_id:
|
||||
repo_id = repo_id.split("github.com/")[1]
|
||||
|
||||
if repo_id in Hacs().common.skip:
|
||||
Hacs().common.skip.remove(repo_id)
|
||||
|
||||
if not Hacs().get_by_name(repo_id):
|
||||
result = await Hacs().register_repository(repo_id, data.lower())
|
||||
if result is not None:
|
||||
result = {"message": str(result), "action": "add_repository"}
|
||||
hass.bus.async_fire("hacs/error", result)
|
||||
|
||||
repository = Hacs().get_by_name(repo_id)
|
||||
else:
|
||||
repository = Hacs().get_by_id(repo_id)
|
||||
|
||||
if repository is None:
|
||||
hass.bus.async_fire("hacs/repository", {})
|
||||
return
|
||||
|
||||
Hacs().logger.info(f"Running {action} for {repository.information.full_name}")
|
||||
|
||||
if action == "set_state":
|
||||
repository.state = data
|
||||
|
||||
elif action == "set_version":
|
||||
repository.state = None
|
||||
repository.status.selected_tag = data
|
||||
await repository.update_repository()
|
||||
|
||||
elif action == "add":
|
||||
repository.state = None
|
||||
|
||||
else:
|
||||
repository.state = None
|
||||
Hacs().logger.error(f"WS action '{action}' is not valid")
|
||||
|
||||
await Hacs().data.async_write()
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{vol.Required("type"): "hacs/check_path", vol.Optional("path"): cv.string}
|
||||
)
|
||||
async def check_local_path(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
path = msg.get("path")
|
||||
exist = {"exist": False}
|
||||
|
||||
if path is None:
|
||||
return
|
||||
|
||||
if os.path.exists(path):
|
||||
exist["exist"] = True
|
||||
|
||||
connection.send_message(websocket_api.result_message(msg["id"], exist))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({vol.Required("type"): "hacs/get_critical"})
|
||||
async def get_critical_repositories(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
critical = await async_load_from_store(hass, "critical")
|
||||
if not critical:
|
||||
critical = []
|
||||
connection.send_message(websocket_api.result_message(msg["id"], critical))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{vol.Required("type"): "hacs/critical", vol.Optional("repository"): cv.string}
|
||||
)
|
||||
async def acknowledge_critical_repository(hass, connection, msg):
|
||||
"""Handle get media player cover command."""
|
||||
repository = msg["repository"]
|
||||
|
||||
critical = await async_load_from_store(hass, "critical")
|
||||
for repo in critical:
|
||||
if repository == repo["repository"]:
|
||||
repo["acknowledged"] = True
|
||||
await async_save_to_store(hass, "critical", critical)
|
||||
connection.send_message(websocket_api.result_message(msg["id"], critical))
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "yandex_maps",
|
||||
"name": "Yandex Maps",
|
||||
"documentation": "https://github.com/custom-components/sensor.yandex_maps",
|
||||
"dependencies": [],
|
||||
"codeowners": [],
|
||||
"requirements": []
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
"""
|
||||
A platform which give you the time it will take to drive.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://github.com/custom-components/sensor.yandex_maps
|
||||
"""
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
__version__ = '0.0.5'
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_START = 'start'
|
||||
CONF_DESTINATION = 'destination'
|
||||
|
||||
ICON = 'mdi:car'
|
||||
|
||||
BASE_URL = 'https://yandex.ru/geohelper/api/v1/router?points={}~{}'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_START): cv.string,
|
||||
vol.Required(CONF_DESTINATION): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None): # pylint: disable=unused-argument
|
||||
"""Setup sensor platform."""
|
||||
name = config['name']
|
||||
start = config['start']
|
||||
destination = config['destination']
|
||||
|
||||
async_add_entities(
|
||||
[YandexMapsSensor(hass, name, start, destination)], True)
|
||||
|
||||
|
||||
class YandexMapsSensor(Entity):
|
||||
"""YandexMap Sensor class"""
|
||||
|
||||
def __init__(self, hass, name, start, destination):
|
||||
self.hass = hass
|
||||
self._state = None
|
||||
self._name = name
|
||||
self._start = start
|
||||
self._destination = destination
|
||||
self.attr = {}
|
||||
_LOGGER.debug('Initialized sensor %s with %s, %s', self._name, self._start, self._destination)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update sensor."""
|
||||
_LOGGER.debug('%s - Running update', self._name)
|
||||
|
||||
try:
|
||||
url = BASE_URL.format(self.start, self.destination)
|
||||
|
||||
_LOGGER.debug('Requesting url %s', url)
|
||||
async with aiohttp.ClientSession() as client:
|
||||
async with client.get(url) as resp:
|
||||
assert resp.status == 200
|
||||
info = await resp.json()
|
||||
|
||||
self._state = info.get('direct', {}).get('time')
|
||||
self.attr = {
|
||||
'mapurl': info.get('direct', {}).get('mapUrl'),
|
||||
'jamsrate': info.get('jamsRate'),
|
||||
'jamsmeasure': info.get('jamsMeasure')
|
||||
}
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
_LOGGER.debug('%s - Could not update - %s', self._name, error)
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self.point_to_coords(self._start)
|
||||
|
||||
@property
|
||||
def destination(self):
|
||||
return self.point_to_coords(self._destination)
|
||||
|
||||
def point_to_coords(self, point):
|
||||
if 'device_tracker' in point or 'zone' in point:
|
||||
state = self.hass.states.get(point)
|
||||
if state:
|
||||
latitude = state.attributes.get('latitude')
|
||||
longitude = state.attributes.get('longitude')
|
||||
if latitude and longitude:
|
||||
return "{},{}".format(longitude, latitude)
|
||||
else:
|
||||
raise AttributeError
|
||||
return point
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""State."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""unit_of_measurement."""
|
||||
return 'мин'
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Attributes."""
|
||||
return self.attr
|
||||
Submodule hass/settings/custom_components/yandex_smart_home deleted from fafbf661b7
Reference in New Issue
Block a user