some new automations

This commit is contained in:
Your Name
2019-11-07 12:36:14 +03:00
parent 7a32cb74da
commit a00aaba52b
116 changed files with 6720 additions and 126 deletions
+1
View File
@@ -6,6 +6,7 @@ filter:
- light
- vacuum
- cover
- fan
include_entities:
- input_boolean.alice_night_mode
- input_boolean.alice_turn_off_light
@@ -0,0 +1,2 @@
fan.xiaomi_miio_device:
friendly_name: Воздухоочиститель
@@ -6,6 +6,9 @@ filter:
- light
- vacuum
- cover
- fan
- climate
- camera
include_entities:
- sensor.humidity_158d0001149b3c
- sensor.humidity_158d000117d37a
@@ -15,6 +18,9 @@ filter:
- sensor.temperature_158d000117d37a
- sensor.temperature_158d00012d9ad7
- sensor.temperature_158d000149c128
- input_boolean.monster_work
- input_boolean.monster_home
- input_boolean.homekit_presense
exclude_entities:
- switch.plug_158d00010dd98d
- switch.xiaomi_miio_switch
@@ -0,0 +1,6 @@
monster_work:
name: Монстр на работе
monster_home:
name: Монстр дома
homekit_presense:
name: Присутсиве по HomeKit
@@ -0,0 +1,21 @@
- platform: mqtt
state_topic: 'home/domofon/$uptime'
name: 'uptime_domofon'
- platform: mqtt
state_topic: 'home/bath/$uptime'
name: 'uptime_bath'
- platform: mqtt
state_topic: 'home/kitchen-led/$uptime'
name: 'uptime_kitchen-led'
- platform: mqtt
state_topic: 'home/nest/$uptime'
name: 'uptime_nest'
- platform: mqtt
state_topic: 'home/airfresher/$uptime'
name: 'uptime_airfresher'
- platform: mqtt
state_topic: 'tele/bathlight/UPTIME'
name: 'uptime_bathlight'
- platform: mqtt
state_topic: 'tele/kitchen/UPTIME'
name: 'uptime_kitchen'
@@ -0,0 +1,11 @@
#################################################
# CAR TRAVEL TIME #
#################################################
- platform: yandex_maps
start: !secret home_coords
destination: !secret work_coords
name: monster_travel_time_home_to_work
- platform: yandex_maps
start: !secret work_coords
destination: !secret home_coords
name: monster_travel_time_work_to_home
Binary file not shown.
@@ -0,0 +1,187 @@
"""
Custom element manager for community created elements.
For more details about this integration, please refer to the documentation at
https://hacs.xyz/
"""
import voluptuous as vol
from aiogithubapi import AIOGitHub
from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.const import __version__ as HAVERSION
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.event import async_call_later
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
from .const import DEV_MODE, DOMAIN, ELEMENT_TYPES, STARTUP, VERSION
from .constrains import check_constans
from .hacsbase import Hacs
from .hacsbase.configuration import Configuration
from .hacsbase.data import HacsData
from .hacsbase.migration import ValidateData
from .setup import add_sensor, load_hacs_repository, setup_frontend
SCHEMA = hacs_base_config_schema()
SCHEMA[vol.Optional("options")] = hacs_config_option_schema()
CONFIG_SCHEMA = vol.Schema({DOMAIN: SCHEMA}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up this integration using yaml."""
if DOMAIN not in config:
return True
hass.data[DOMAIN] = config
Hacs.hass = hass
Hacs.configuration = Configuration.from_dict(
config[DOMAIN], config[DOMAIN].get("options")
)
Hacs.configuration.config_type = "yaml"
await startup_wrapper_for_yaml(Hacs)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={}
)
)
return True
async def async_setup_entry(hass, config_entry):
"""Set up this integration using UI."""
conf = hass.data.get(DOMAIN)
if config_entry.source == config_entries.SOURCE_IMPORT:
if conf is None:
hass.async_create_task(
hass.config_entries.async_remove(config_entry.entry_id)
)
return False
Hacs.hass = hass
Hacs.configuration = Configuration.from_dict(
config_entry.data, config_entry.options
)
Hacs.configuration.config_type = "flow"
Hacs.configuration.config_entry = config_entry
config_entry.add_update_listener(reload_hacs)
startup_result = await hacs_startup(Hacs)
if not startup_result:
raise ConfigEntryNotReady
return startup_result
async def startup_wrapper_for_yaml(hacs):
"""Startup wrapper for yaml config."""
startup_result = await hacs_startup(hacs)
if not startup_result:
hacs.hass.components.frontend.async_remove_panel(
hacs.configuration.sidepanel_title.lower()
.replace(" ", "_")
.replace("-", "_")
)
hacs.logger.info("Could not setup HACS, trying again in 15 min")
async_call_later(hacs.hass, 900, startup_wrapper_for_yaml(hacs))
async def hacs_startup(hacs):
"""HACS startup tasks."""
hacs.logger.debug(f"Configuration type: {hacs.configuration.config_type}")
hacs.version = VERSION
hacs.logger.info(STARTUP)
hacs.system.config_path = hacs.hass.config.path()
hacs.system.ha_version = HAVERSION
hacs.system.disabled = False
hacs.github = AIOGitHub(
hacs.configuration.token, async_create_clientsession(hacs.hass)
)
hacs.data = HacsData()
# Check HACS Constrains
if not check_constans(hacs):
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
# Set up frontend
await setup_frontend(hacs)
# Load HACS
if not await load_hacs_repository(hacs):
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
val = ValidateData()
if not val.validate_local_data_file():
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
# Restore from storefiles
if not await hacs.data.restore():
hacs_repo = hacs().get_by_name("hacs/integration")
hacs_repo.pending_restart = True
if hacs.configuration.config_type == "flow":
if hacs.configuration.config_entry is not None:
await async_remove_entry(hacs.hass, hacs.configuration.config_entry)
return False
# Add aditional categories
if hacs.configuration.appdaemon:
ELEMENT_TYPES.append("appdaemon")
if hacs.configuration.python_script:
ELEMENT_TYPES.append("python_script")
if hacs.configuration.theme:
ELEMENT_TYPES.append("theme")
hacs.common.categories = sorted(ELEMENT_TYPES)
# Setup startup tasks
if hacs.configuration.config_type == "yaml":
hacs.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, hacs().startup_tasks()
)
else:
async_call_later(hacs.hass, 5, hacs().startup_tasks())
# Print DEV warning
if hacs.configuration.dev:
hacs.logger.warning(DEV_MODE)
hacs.hass.components.persistent_notification.create(
title="HACS DEV MODE", message=DEV_MODE, notification_id="hacs_dev_mode"
)
# Add sensor
add_sensor(hacs)
# Set up services
# await add_services(hacs)
# Mischief managed!
return True
async def async_remove_entry(hass, config_entry):
"""Handle removal of an entry."""
Hacs().logger.info("Disabling HACS")
Hacs().logger.info("Removing recuring tasks")
for task in Hacs().tasks:
task()
Hacs().logger.info("Removing sensor")
try:
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
except ValueError:
pass
Hacs().logger.info("Removing sidepanel")
try:
hass.components.frontend.async_remove_panel("hacs_web")
except AttributeError:
pass
Hacs().system.disabled = True
Hacs().logger.info("HACS is now disabled")
async def reload_hacs(hass, config_entry):
"""Reload HACS."""
await async_remove_entry(hass, config_entry)
await async_setup_entry(hass, config_entry)
+421
View File
@@ -0,0 +1,421 @@
"""API Endpoins."""
from time import time
from aiohttp import web
from integrationhelper import Logger
from .http import HacsWebResponse
APIRESPONSE = {}
def apiresponse(classname):
"""Decorator used to register API Responses."""
APIRESPONSE[classname.name] = classname
return classname
class HacsAPI(HacsWebResponse):
"""HacsAPI class."""
name = "hacsapi"
def __init__(self):
"""Initialize."""
self.logger = Logger("hacs.api")
self.url = self.hacsapi + "/{endpoint}"
async def post(self, request, endpoint): # pylint: disable=unused-argument
"""Handle HACS API requests."""
if self.system.disabled:
return web.Response(status=404)
self.endpoint = endpoint
self.postdata = await request.post()
self.raw_headers = request.raw_headers
self.request = request
self.logger.debug(f"Endpoint ({endpoint}) called")
if self.configuration.dev:
self.logger.debug(f"Raw headers ({self.raw_headers})")
self.logger.debug(f"Postdata ({self.postdata})")
if self.endpoint in APIRESPONSE:
try:
response = APIRESPONSE[self.endpoint]
response = await response.response(self)
except Exception as exception:
render = self.render(f"error", message=exception)
return web.Response(
body=render, content_type="text/html", charset="utf-8"
)
else:
# Return default response.
response = await APIRESPONSE["generic"].response(self)
# set headers
response.headers["Cache-Control"] = "max-age=0, must-revalidate"
# serve the response
return response
class HacsRunningTask(HacsAPI):
"""Return if BG task is running."""
name = "hacs:task"
def __init__(self):
"""Initialize."""
self.url = "/hacs_task"
async def get(self, request): # pylint: disable=unused-argument
"""Handle GET request."""
return web.json_response({"task": self.system.status.background_task})
@apiresponse
class Generic(HacsAPI):
"""Generic API response."""
name = "generic"
async def response(self):
"""Response."""
self.logger.error(f"Unknown endpoint '{self.endpoint}'")
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RemoveNewFlag(HacsAPI):
"""Remove new flag on all repositories."""
name = "remove_new_flag"
async def response(self):
"""Response."""
for repository in self.repositories:
repository.status.new = False
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/{self.postdata['return']}?timestamp={time()}"
)
@apiresponse
class DevTemplate(HacsAPI):
"""Remove new flag on all repositories."""
name = "dev_template"
async def response(self):
"""Response."""
from .handler.template import render_template
if "set" in self.postdata:
self.developer.template_id = self.postdata.get("repository_id")
repository = self.get_by_id(self.developer.template_id)
template = render_template(self.postdata.get("template", ""), repository)
info = await self.github.render_markdown(template)
info = info.replace("<h3>", "<h6>").replace("</h3>", "</h6>")
info = info.replace("<h2>", "<h5>").replace("</h2>", "</h5>")
info = info.replace("<h1>", "<h4>").replace("</h1>", "</h4>")
info = info.replace("<code>", "<code class='codeinfo'>")
info = info.replace(
'<a href="http', '<a rel="noreferrer" target="_blank" href="http'
)
info = info.replace("<ul>", "")
info = info.replace("</ul>", "")
self.developer.template_content = info
else:
self.developer.template_content = ""
self.developer.template_id = "Repository ID"
render = self.render("settings/dev/template_test")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@apiresponse
class DevView(HacsAPI):
"""Set HA version view."""
name = "devview"
async def response(self):
"""Response."""
render = self.render(f"settings/dev/{self.postdata['view']}")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@apiresponse
class SetHAVersionAction(HacsAPI):
"""Set HA version action."""
name = "set_ha_version_action"
async def response(self):
"""Response."""
self.common.ha_version = self.postdata["ha_version"]
render = self.render("settings/dev/set_ha_version")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@apiresponse
class RepositoryInstall(HacsAPI):
"""Install repository."""
name = "repository_install"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.install()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoryUpdate(HacsAPI):
"""Update repository."""
name = "repository_update"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.update_repository()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoryUninstall(HacsAPI):
"""Uninstall repository."""
name = "repository_uninstall"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.uninstall()
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/overview?timestamp={time()}")
@apiresponse
class RepositoryRemove(HacsAPI):
"""Remove repository."""
name = "repository_remove"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
await repository.remove()
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoryHide(HacsAPI):
"""Hide repository."""
name = "repository_hide"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.hide = True
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/store?timestamp={time()}")
@apiresponse
class RepositoryUnhide(HacsAPI):
"""Unhide repository."""
name = "repository_unhide"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.hide = False
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoryBetaHide(HacsAPI):
"""Hide Beta repository."""
name = "repository_beta_hide"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.status.show_beta = False
await repository.update_repository()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoryBetaShow(HacsAPI):
"""Show Beta repository."""
name = "repository_beta_show"
async def response(self):
"""Response."""
repository = self.get_by_id(self.postdata["repository_id"])
repository.status.show_beta = True
await repository.update_repository()
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class RepositoriesReload(HacsAPI):
"""Reload repository data."""
name = "repositories_reload"
async def response(self):
"""Response."""
self.hass.async_create_task(self.recuring_tasks_all())
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoriesUpgradeAll(HacsAPI):
"""Upgrade all repositories."""
name = "repositories_upgrade_all"
async def response(self):
"""Response."""
for repository in self.repositories:
if repository.pending_upgrade:
await repository.install()
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@apiresponse
class RepositoryRegister(HacsAPI):
"""Register repository."""
name = "repository_register"
async def response(self):
"""Response."""
repository_name = self.postdata.get("custom_url")
repository_type = self.postdata.get("repository_type")
# Validate data
if not repository_name:
message = "Repository URL is missing."
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
if repository_type is None:
message = "Type is missing for '{}'.".format(repository_name)
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
# Stip first part if it's an URL.
if "github" in repository_name:
repository_name = repository_name.split("github.com/")[-1]
# Strip whitespace
repository_name = repository_name.split()[0]
# If it still have content, continue.
if repository_name != "":
if len(repository_name.split("/")) != 2:
message = f"""
{repository_name} is not a valid format.
Correct format is 'https://github.com/DEVELOPER/REPOSITORY'
or 'DEVELOPER/REPOSITORY'.
"""
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
is_known_repository = self.is_known(repository_name)
if is_known_repository:
message = f"'{repository_name}' is already registered, look for it in the store."
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
if repository_name in self.common.blacklist:
self.common.blacklist.remove(repository_name)
await self.register_repository(repository_name, repository_type)
repository = self.get_by_name(repository_name)
if repository is not None:
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
message = f"""
Could not add '{repository_name}' with type '{repository_type}' at this time.</br>
If you used the correct type, check the log for more details."""
return web.HTTPFound(
f"/hacsweb/{self.token}/settings?timestamp={time()}&message={message}"
)
@apiresponse
class RepositorySelectTag(HacsAPI):
"""Select tag for Repository."""
name = "repository_select_tag"
async def response(self):
"""Response."""
from aiogithubapi import AIOGitHubException
from .hacsbase.exceptions import HacsRequirement
repository = self.get_by_id(self.postdata["repository_id"])
if self.postdata["selected_tag"] == repository.releases.last_release:
repository.status.selected_tag = None
else:
repository.status.selected_tag = self.postdata["selected_tag"]
try:
await repository.update_repository()
except (AIOGitHubException, HacsRequirement):
repository.status.selected_tag = repository.releases.last_release
await repository.update_repository()
message = "The version {} is not valid for use with HACS.".format(
self.postdata["selected_tag"]
)
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}&message={message}"
)
await self.data.async_write()
return web.HTTPFound(
f"/hacsweb/{self.token}/repository/{repository.information.uid}?timestamp={time()}"
)
@apiresponse
class FrontentMode(HacsAPI):
"""Set the frontend mode."""
name = "frontend_mode"
async def response(self):
"""Response."""
self.configuration.frontend_mode = self.postdata["view_type"]
await self.data.async_write()
return web.HTTPFound(f"/hacsweb/{self.token}/settings?timestamp={time()}")
@@ -0,0 +1,100 @@
"""Adds config flow for HACS."""
# pylint: disable=dangerous-default-value
import logging
import voluptuous as vol
from aiogithubapi import AIOGitHub, AIOGitHubException, AIOGitHubAuthentication
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN
from .configuration_schema import hacs_base_config_schema, hacs_config_option_schema
_LOGGER = logging.getLogger(__name__)
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for HACS."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize."""
self._errors = {}
async def async_step_user(self, user_input={}):
"""Handle a flow initialized by the user."""
self._errors = {}
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if self.hass.data.get(DOMAIN):
return self.async_abort(reason="single_instance_allowed")
if user_input is not None:
if await self._test_token(user_input["token"]):
return self.async_create_entry(title="", data=user_input)
self._errors["base"] = "auth"
return await self._show_config_form(user_input)
return await self._show_config_form(user_input)
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(hacs_base_config_schema(user_input)),
errors=self._errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return HacsOptionsFlowHandler(config_entry)
async def async_step_import(self, user_input):
"""Import a config entry.
Special type of import, we're not actually going to store any data.
Instead, we're going to rely on the values that are in config file.
"""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="configuration.yaml", data={})
async def _test_token(self, token):
"""Return true if token is valid."""
try:
session = aiohttp_client.async_get_clientsession(self.hass)
client = AIOGitHub(token, session)
await client.get_repo("hacs/org")
return True
except (AIOGitHubException, AIOGitHubAuthentication):
pass
return False
class HacsOptionsFlowHandler(config_entries.OptionsFlow):
"""HACS config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
hacs_config_option_schema(self.config_entry.options)
),
)
@@ -0,0 +1,49 @@
"""HACS Configuration Schemas."""
# pylint: disable=dangerous-default-value
import voluptuous as vol
from .const import LOCALE
# Configuration:
TOKEN = "token"
SIDEPANEL_TITLE = "sidepanel_title"
SIDEPANEL_ICON = "sidepanel_icon"
APPDAEMON = "appdaemon"
PYTHON_SCRIPT = "python_script"
THEME = "theme"
# Options:
COUNTRY = "country"
RELEASE_LIMIT = "release_limit"
EXPERIMENTAL = "experimental"
def hacs_base_config_schema(config: dict = {}) -> dict:
"""Return a shcema configuration dict for HACS."""
if not config:
config = {
TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
SIDEPANEL_TITLE: "Community",
SIDEPANEL_ICON: "mdi:alpha-c-box",
APPDAEMON: False,
PYTHON_SCRIPT: False,
THEME: False,
}
return {
vol.Required(TOKEN, default=config.get(TOKEN)): str,
vol.Optional(SIDEPANEL_TITLE, default=config.get(SIDEPANEL_TITLE)): str,
vol.Optional(SIDEPANEL_ICON, default=config.get(SIDEPANEL_ICON)): str,
vol.Optional(APPDAEMON, default=config.get(APPDAEMON)): bool,
vol.Optional(PYTHON_SCRIPT, default=config.get(PYTHON_SCRIPT)): bool,
vol.Optional(THEME, default=config.get(THEME)): bool,
}
def hacs_config_option_schema(options: dict = {}) -> dict:
"""Return a shcema for HACS configuration options."""
if not options:
options = {COUNTRY: "ALL", RELEASE_LIMIT: 5, EXPERIMENTAL: False}
return {
vol.Optional("country", default=options.get(COUNTRY)): vol.In(LOCALE),
vol.Optional(RELEASE_LIMIT, default=options.get(RELEASE_LIMIT)): int,
vol.Optional(EXPERIMENTAL, default=options.get(EXPERIMENTAL)): bool,
}
@@ -0,0 +1,295 @@
"""Constants for HACS"""
NAME_LONG = "HACS (Home Assistant Community Store)"
NAME_SHORT = "HACS"
VERSION = "0.16.3"
DOMAIN = "hacs"
PROJECT_URL = "https://github.com/hacs/integration/"
CUSTOM_UPDATER_LOCATIONS = [
"{}/custom_components/custom_updater.py",
"{}/custom_components/custom_updater/__init__.py",
]
ISSUE_URL = f"{PROJECT_URL}issues"
DOMAIN_DATA = f"{NAME_SHORT.lower()}_data"
ELEMENT_TYPES = ["integration", "plugin"]
IFRAME = {
"title": "Community",
"icon": "mdi:alpha-c-box",
"url": "/community_overview",
"path": "community",
"require_admin": True,
}
# Messages
CUSTOM_UPDATER_WARNING = """
This cannot be used with custom_updater.
To use this you need to remove custom_updater form {}
"""
DEV_MODE = "You have 'dev' enabled for HACS, this is not intended for regular use, no support will be given if you break something."
STARTUP = f"""
-------------------------------------------------------------------
HACS (Home Assistant Community Store)
Version: {VERSION}
This is a custom integration
If you have any issues with this you need to open an issue here:
https://github.com/hacs/integration/issues
-------------------------------------------------------------------
"""
LOCALE = [
"ALL",
"AF",
"AL",
"DZ",
"AS",
"AD",
"AO",
"AI",
"AQ",
"AG",
"AR",
"AM",
"AW",
"AU",
"AT",
"AZ",
"BS",
"BH",
"BD",
"BB",
"BY",
"BE",
"BZ",
"BJ",
"BM",
"BT",
"BO",
"BQ",
"BA",
"BW",
"BV",
"BR",
"IO",
"BN",
"BG",
"BF",
"BI",
"KH",
"CM",
"CA",
"CV",
"KY",
"CF",
"TD",
"CL",
"CN",
"CX",
"CC",
"CO",
"KM",
"CG",
"CD",
"CK",
"CR",
"HR",
"CU",
"CW",
"CY",
"CZ",
"CI",
"DK",
"DJ",
"DM",
"DO",
"EC",
"EG",
"SV",
"GQ",
"ER",
"EE",
"ET",
"FK",
"FO",
"FJ",
"FI",
"FR",
"GF",
"PF",
"TF",
"GA",
"GM",
"GE",
"DE",
"GH",
"GI",
"GR",
"GL",
"GD",
"GP",
"GU",
"GT",
"GG",
"GN",
"GW",
"GY",
"HT",
"HM",
"VA",
"HN",
"HK",
"HU",
"IS",
"IN",
"ID",
"IR",
"IQ",
"IE",
"IM",
"IL",
"IT",
"JM",
"JP",
"JE",
"JO",
"KZ",
"KE",
"KI",
"KP",
"KR",
"KW",
"KG",
"LA",
"LV",
"LB",
"LS",
"LR",
"LY",
"LI",
"LT",
"LU",
"MO",
"MK",
"MG",
"MW",
"MY",
"MV",
"ML",
"MT",
"MH",
"MQ",
"MR",
"MU",
"YT",
"MX",
"FM",
"MD",
"MC",
"MN",
"ME",
"MS",
"MA",
"MZ",
"MM",
"NA",
"NR",
"NP",
"NL",
"NC",
"NZ",
"NI",
"NE",
"NG",
"NU",
"NF",
"MP",
"NO",
"OM",
"PK",
"PW",
"PS",
"PA",
"PG",
"PY",
"PE",
"PH",
"PN",
"PL",
"PT",
"PR",
"QA",
"RO",
"RU",
"RW",
"RE",
"BL",
"SH",
"KN",
"LC",
"MF",
"PM",
"VC",
"WS",
"SM",
"ST",
"SA",
"SN",
"RS",
"SC",
"SL",
"SG",
"SX",
"SK",
"SI",
"SB",
"SO",
"ZA",
"GS",
"SS",
"ES",
"LK",
"SD",
"SR",
"SJ",
"SZ",
"SE",
"CH",
"SY",
"TW",
"TJ",
"TZ",
"TH",
"TL",
"TG",
"TK",
"TO",
"TT",
"TN",
"TR",
"TM",
"TC",
"TV",
"UG",
"UA",
"AE",
"GB",
"US",
"UM",
"UY",
"UZ",
"VU",
"VE",
"VN",
"VG",
"VI",
"WF",
"EH",
"YE",
"ZM",
"ZW",
]
@@ -0,0 +1,44 @@
"""HACS Startup constrains."""
# pylint: disable=bad-continuation
import os
import json
from .const import CUSTOM_UPDATER_LOCATIONS, CUSTOM_UPDATER_WARNING
from .helpers.misc import version_is_newer_than_version
def check_constans(hacs):
"""Check HACS constrains."""
if not constrain_custom_updater(hacs):
return False
if not constrain_version(hacs):
return False
return True
def constrain_custom_updater(hacs):
"""Check if custom_updater exist."""
for location in CUSTOM_UPDATER_LOCATIONS:
if os.path.exists(location.format(hacs.system.config_path)):
msg = CUSTOM_UPDATER_WARNING.format(
location.format(hacs.system.config_path)
)
hacs.logger.critical(msg)
return False
return True
def constrain_version(hacs):
"""Check if the version is valid."""
with open(
f"{hacs.system.config_path}/custom_components/hacs/manifest.json", "r"
) as read:
manifest = json.loads(read.read())
# Check if HA is the required version.
if version_is_newer_than_version(manifest["homeassistant"], hacs.system.ha_version):
hacs.logger.critical(
f"You need HA version {manifest['homeassistant']} or newer to use this integration."
)
return False
return True
@@ -0,0 +1,49 @@
"""Deprecated functions, should be fine to remove."""
async def add_services(hacs):
"""Add services."""
# Service registration
async def service_hacs_install(call):
"""Install a repository."""
repository = str(call.data["repository"])
if repository not in hacs().store.repositories:
hacs.logger.error("%s is not a konwn repository!", repository)
return
repository = hacs().store.repositories[repository]
await repository.install()
async def service_hacs_register(call):
"""register a repository."""
repository = call.data["repository"]
repository_type = call.data["repository_type"]
if await hacs().is_known_repository(repository):
hacs.logger.error("%s is already a konwn repository!", repository)
return
await hacs().register_new_repository(repository_type, repository)
async def service_hacs_load(call):
"""register a repository."""
from homeassistant.loader import async_get_custom_components
del hacs.hass.data["custom_components"]
await async_get_custom_components(hacs.hass)
hacs.hass.services.async_register("hacs", "install", service_hacs_install)
hacs.hass.services.async_register("hacs", "register", service_hacs_register)
hacs.hass.services.async_register("hacs", "load", service_hacs_load)
# This should probably be in a dedicated test "package"
async def test_repositories(hacs):
"""Test repositories."""
await hacs().register_repository("ludeeus/theme-hacs", "theme")
await hacs().register_repository("ludeeus/ps-hacs", "python_script")
await hacs().register_repository("ludeeus/integration-hacs", "integration")
await hacs().register_repository(
"rgruebel/ha_zigbee2mqtt_networkmap", "integration"
)
await hacs().register_repository("ludeeus/ad-hacs", "appdaemon")
await hacs().register_repository("jonkristian/entur-card", "plugin") # Dist
await hacs().register_repository("kalkih/mini-media-player", "plugin") # Release
await hacs().register_repository("custom-cards/monster-card", "plugin") # root
@@ -0,0 +1,504 @@
.yaml {
font-family: monospace, monospace;
font-size: 1em;
border-style: solid;
border-width: thin;
margin: 0;
overflow: auto;
display: inline-flex;
width: calc(100% - 20px);
white-space: pre-wrap;
}
.hacslist {
border: none;
}
.hacs-card-container {
display: grid;
grid-gap: 10px;
}
.hacs-overview-container {
width: 95%;
margin-left: 2.5%;
margin-right: 2.5%;
margin-bottom: 10px;
display: inline-block;
}
.hacs-form {
display: contents;
}
.hacs-table {
background-color: var(--paper-card-background-color);
}
.hacs-content {
position: absolute;
top: 49px;
left: 0px;
right: 0px;
bottom: 0px;
overflow-y: auto;
overflow-x: hidden;
padding-top: 20px;
padding-bottom: 15px;
}
.hacs-card {
height: 100%;
background-color: var(--paper-card-background-color) !important;
padding: 3px;
line-height: 1;
}
.overview {
height: 100%;
}
.progressbar {
margin-left: 2.5%;
margin-right: 2.5%;
margin-bottom: 20px;
}
.hacs-card-title {
font-size: medium;
color: var(--primary-text-color);
}
tr.hacs-table-row.repository {
border-bottom: 0px;
display: table-row-group;
}
td.repository {
width: 50%;
padding: 0px;
}
td.repository.text {
padding-left: 12px;
}
select.hacs-select.repository {
width: 100%;
}
.hacs-card-content {
color: var(--secondary-text-color);
font-style: italic;
font-size: small;
}
.card-status {
font-size: 18px;
padding-right: 4px;
}
.hacs-card-standalone {
background-color: var(--paper-card-background-color) !important;
color: var(--primary-text-color);
padding: 16px;
}
.installed {
color: var(--hacs-status-installed);
}
.pending-restart {
color: var(--hacs-status-pending-restart);
}
.pending-upgrade {
color: var(--hacs-status-pending-update);
}
.default {
color: var(--primary-text-color);
}
.hacs-nav {
background-color: var(--primary-color) !important;
position: fixed;
height: auto;
}
a {
color: var(--primary-color);
}
.disabledaction {
cursor: default;
color: var(--primary-color);
opacity: 0.5;
}
.hacs-bar {
background-color: var(--primary-color) !important;
}
.hacs-bar-background {
background-color: var(--dark-primary-color) !important;
}
.errorview {
display: block;
margin-bottom: 30px;
}
.hacscolor {
background-color: var(--paper-card-background-color) !important;
color: var(--primary-text-color);
}
.hacsbutton {
margin: 3%;
background-color: var(--primary-color) !important;
}
.hacsadminbutton {
margin: 1%;
background-color: var(--primary-color) !important;
}
body {
background-color: var(--primary-background-color) !important;
color: var(--primary-text-color);
overflow: hidden;
font-size: 14px;
}
input[type="text"]:not(.browser-default):focus:not([readonly]) {
border-bottom: 1px solid var(--primary-color);
-webkit-box-shadow: 0 1px 0 0 var(--primary-color);
box-shadow: 0 1px 0 0 var(--primary-color);
}
.dropdown-list-item {
color: var(--primary-color) !important;
background-color: var(--paper-card-background-color) !important;
}
a.dropdown-list-item:hover {
opacity: 0.95;
}
.actionlink {
color: var(--primary-color) !important
}
.uninstall {
float: right;
color: var(--google-red-500) !important;
font-weight: bold;
}
@media all and (max-width: 550px) {
select.hacs-select.custom_repository {
width: 100%;
}
input#custom_url {
width: 100% !important;
}
.actionlink {
width: 100%;
padding: 5px;
display: block;
font-size: x-large;
text-align: center;
}
.uninstall {
float: none;
}
}
.minigrid {
display: contents;
}
.card-action a:hover {
opacity: 0.6 !important;
}
.codeinfo {
opacity: 0.6 !important;
}
.hacs-select {
display: initial !important;
margin-left: 1%;
width: 20%;
background-color: transparent;
border-color: transparent;
color: var(--primary-text-color);
}
.hacs-badge {
background-color: var(--hacs-badge-color);
margin-right: 1%;
font-size: 12px;
padding: 0px 4px 2px 4px;
color: var(--hacs-badge-text-color);
-moz-border-radius: 10px 10px 10px 10px;
border-radius: 10px 10px 10px 10px;
}
tr.hacs-table-row:hover {
cursor: pointer;
}
/* color vars*/
:root {
--hacs-status-installed: #126e15;
--hacs-status-pending-restart: #a70000;
--hacs-status-pending-update: #ffab40;
--hacs-status-default: var(--primary-text-color);
--hacs-badge-color: var(--primary-color);
--hacs-badge-text-color: var(--primary-text-color);
--primary-color: #03a9f4;
--primary-text-color: #000000;
--secondary-text-color: #424141;
--primary-background-color: #fafafa;
--paper-card-background-color: #ffffff;
--dark-primary-color: #00405d;
--google-red-500: #db4437;
}
/* hug screens*/
@media all and (min-width: 2000px) {
.hacs-card-container {
grid-template-columns: repeat(6, 1fr);
}
}
/* even bigger screens*/
@media all and (min-width: 1500px) and (max-width: 1999px) {
.hacs-card-container {
grid-template-columns: repeat(5, 1fr);
}
}
/* bigger screens*/
@media all and (min-width: 1000px) and (max-width: 1499px) {
.hacs-card-container {
grid-template-columns: repeat(4, 1fr);
}
}
/* big screens */
@media all and (min-width: 550px) and (max-width: 999px) {
.hacs-card-container {
grid-template-columns: repeat(3, 1fr);
}
#main::-webkit-scrollbar-track
{
border-radius: 10px;
}
#main::-webkit-scrollbar
{
width: 12px;
}
#main::-webkit-scrollbar-thumb
{
border-radius: 10px;
background-color: var(--primary-color);
}
}
/* smal screens */
@media all and (max-width: 550px) {
body {
overflow: scroll !important;
}
.minigrid {
display: grid;
}
.smal-hide {
display: none !important;
}
.hacs-content {
overflow: scroll !important;
-webkit-overflow-scrolling: touch;
}
}
/* laoder */
*.hidden {
display: none !important;
}
div.loading{
position: fixed;
z-index: 999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(16, 16, 16, 0.5);
}
@-webkit-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-ms-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-moz-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-o-keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes uil-ring-anim {
0% {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.uil-ring-css {
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 200px;
height: 200px;
}
.uil-ring-css > div {
position: absolute;
display: block;
width: 160px;
height: 160px;
top: 20px;
left: 20px;
border-radius: 80px;
box-shadow: 0 6px 0 0 var(--primary-color);
-ms-animation: uil-ring-anim 1s linear infinite;
-moz-animation: uil-ring-anim 1s linear infinite;
-webkit-animation: uil-ring-anim 1s linear infinite;
-o-animation: uil-ring-anim 1s linear infinite;
animation: uil-ring-anim 1s linear infinite;
}
@@ -0,0 +1,111 @@
// Copy active HA theme
document.getElementsByTagName("html").item(0).setAttribute("style", parent.document.getElementsByTagName("html").item(0).style.cssText)
// Copy yhe content of the Lovelace example to the clipboard.
function CopyToLovelaceExampleToClipboard() {
window.getSelection().selectAllChildren(document.getElementById("LovelaceExample"));
document.execCommand("copy");
document.getSelection().empty()
document.getElementById('lovelacecopy').style.color = 'forestgreen';
}
// Show progress bar
function ShowProgressBar() {
document.getElementById('progressbar').style.display = 'block';
}
// Hard reloading
function HardReload() {
parent.location.reload(true)
}
// Searchbar
function Search() {
var input = document.getElementById("Search");
if (input) {
var filter = input.value.toLowerCase();
var nodes = document.getElementsByClassName('hacs-card');
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].innerHTML.toLowerCase().includes(filter)) {
nodes[i].style.display = "block";
} else {
nodes[i].style.display = "none";
}
}
var nodes = document.getElementsByClassName('hacs-table-row');
for (i = 0; i < nodes.length; i++) {
if (nodes[i].innerHTML.toLowerCase().includes(filter)) {
nodes[i].style.display = "table-row";
} else {
nodes[i].style.display = "none";
}
}
}
}
// Dropdown
document.addEventListener('DOMContentLoaded', function () {
var elems = document.querySelectorAll('.dropdown-trigger');
M.Dropdown.init(elems, { hover: false, constrainWidth: false });
});
// Modal
document.addEventListener('DOMContentLoaded', function () {
var elems = document.querySelectorAll('.modal');
M.Modal.init(elems, {});
});
// Loader
function toggleLoading() {
var loadingOverlay = document.querySelector('.loading');
loadingOverlay.classList.remove('hidden')
document.activeElement.blur();
}
// Check if we can reload
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
function CheckIfWeCanReload() {
var data = true;
const hacsrequest = new XMLHttpRequest()
hacsrequest.open('GET', '/hacs_task', true)
hacsrequest.onload = function () {
data = JSON.parse(this.response)
data = data["task"]
if (!data) {
console.log("Background task is no longer running, reloading in 5s...")
sleep(5000).then(() => {
location.reload()
});
}
}
hacsrequest.send()
}
function IsTaskRunning() {
let retval = false;
let disp = document.getElementsByClassName("progress")
if (disp) {
disp = disp.item(0).style.display;
if (disp == "block") {
retval = true
} else {
retval = false
}
}
return retval
}
window.setInterval(function () {
var running = false;
running = IsTaskRunning();
if (running) {
CheckIfWeCanReload();
}
}, 10000);
@@ -0,0 +1,51 @@
{% set progressbar = 'block' if hacs.system.status.background_task else 'none' %}
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/materialize.min.css.gz">
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/all.min.css.gz">
<script src="/hacsweb/{{ hacs.token }}/static/materialize.min.js.gz"></script>
<link rel="stylesheet" href="/hacsweb/{{ hacs.token }}/static/static/hacs.css">
<script src="/hacsweb/{{ hacs.token }}/static/hacs.js"></script>
</head>
<body>
<div class="navbar-fixed">
<nav class="nav-extended hacs-nav">
<div class="nav-content">
<ul class="right tabs tabs-transparent">
<li class="tab {{ 'active' if location == 'overview' }}"><a href="/hacsweb/{{ hacs.token }}/overview?timestamp={{ timestamp }}">Overview</a></li>
<li class="tab {{ 'active' if location == 'store' }}"><a href="/hacsweb/{{ hacs.token }}/store?timestamp={{ timestamp }}">Store</a></li>
<li class="tab right {{ 'active' if location == 'settings' }}"><a href="/hacsweb/{{ hacs.token }}/settings?timestamp={{ timestamp }}">Settings</a></li>
</ul>
</div>
</nav>
</div>
<div id="main" class="hacs-content">
<div class="progressbar" style="display: {{ progressbar }}">
<p>Background task running, this page will reload when it's done.</p>
<div class="progress hacs-bar-background" id="progressbar">
<div class="indeterminate hacs-bar"></div>
</div>
</div>
<div class="loading hidden">
<div class='uil-ring-css' style='transform:scale(0.79);'>
<div></div>
</div>
</div>
{% if message %}
{% include 'message.html' with context %}
{% endif %}
{% set hacs_repo = hacs.get_by_name("hacs/integration") %}
{% if hacs_repo.pending_restart %}
{% set message = "The frontend of HACS has been disabled until you restart Home Assistant." %}
{% include 'message.html' with context %}
{% else %}
{% block content %}
{% endblock %}
{% endif %}
</div>
</body>
</html>
@@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block content %}
{% endblock %}
@@ -0,0 +1,36 @@
<div class='hacs-overview-container'>
<h5>{{ displaytype }}</h5>
<div class='hacs-card-container'>
{% for repository in repositories.repositories %}
<a href="/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}"
class="hacs-card" onclick="toggleLoading()">
<div class="hacs-card overview">
<div id="statusicon">
</div>
<div id="title">
<input type="hidden" name="author" value="{{repository.information.authors}}">
<input type="hidden" name="topics" value="{{repository.information.topics}}">
<input type="hidden" name="full_name" value="{{repository.information.full_name}}">
<span class="hacs-card-title">
{% if location == 'store' and repository.status.new %}
<span title="{{ repository.display_status_description }}" class='hacs-badge'>NEW</span>
{% else %}
<i title="{{ repository.display_status_description }}"
class='fas fa-cube card-status {{repository.display_status}}'></i>
{% endif %}
{{repository.display_name}}
</span>
</div>
<div id="description">
<span class="hacs-card-content">
<p>{{repository.information.description}}</p>
</span>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
</br>
</br>
@@ -0,0 +1,47 @@
{% set repositories = namespace(repositories=[]) %}
{% for repository in hacs.sorted_by_name %}
{% if repository.status.new %}
{% set repositories.repositories = repositories.repositories+[repository] %}
{% endif %}
{% endfor %}
{% if repositories.repositories %}
<div class='hacs-overview-container'>
<h5>NEW REPOSITORIES (ALL)</h5>
<form action="/hacsapi/{{ hacs.token }}/remove_new_flag" name="remove_new_flag" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="return" value="store">
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
onclick="toggleLoading();document.getElementsByName('remove_new_flag')[0].submit()" style="width: 95%;">
Remove the NEW flag for all new items
</a>
</form>
<div class='hacs-card-container'>
{% for repository in repositories.repositories %}
<a href="/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}"
class="hacs-card" onclick="toggleLoading()">
<div class="hacs-card overview">
<div id="statusicon">
</div>
<div id="title">
<span class="hacs-card-title">
<span class="hacs-badge">
{{repository.information.category | upper}}
</span>
</br></br>
{{repository.display_name}}
</span>
</div>
<div id="description">
<span class="hacs-card-content">
<p>{{repository.information.description}}</p>
</span>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
</br>
</br>
{% endif %}
@@ -0,0 +1,13 @@
<div class='container'>
<div class="row">
<div class="col s12">
<div class="card-panel orange darken-4">
<div class="card-content white-text">
<span>
{{message}}
</span>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,33 @@
<div id="modal_upgrade_all" class="modal hacscolor">
<div class="modal-content">
<h5>Pending Upgrades</h5>
{% for repository in hacs.repositories %}
{% if repository.pending_upgrade %}
<p>- {{repository.information.name}} (
{{repository.display_installed_version}}
->
{{repository.display_available_version}}
)</p>
{% endif %}
{% endfor %}
</br>
<p>
Be careful using this feature, elements may contain breaking changes,
make sure you read the release notes for all the elements in the list above.
</p>
</div>
<div class="modal-footer hacscolor">
{% if hacs.system.status.background_task %}
<p>Background task is running, batch upgrade is disabled.</p>
{% else %}
<form action="/hacsapi/{{ hacs.token }}/repositories_upgrade_all" name="upgrade_all" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
<a href="#" onclick="toggleLoading();document.getElementsByName('upgrade_all')[0].submit()"
class='waves-effect waves-light btn hacsbutton'
style="background-color: var(--google-red-500) !important; font-weight: bold;">
UPGRADE ALL
</a>
</form>
{% endif %}
</div>
</div>
@@ -0,0 +1,12 @@
<div id="haversion" class="modal hacscolor">
<div class="modal-content">
<h5>Unsupported Home Assistant version</h5>
<p>
You have version '{{hacs.system.ha_version}}' of Home Assistant, but version
'{{repository.display_available_version}}' of '{{repository.display_name}}' require version
'{{repository.repository_manifest.homeassistant}}' of Home Assistant, installation and upgrades are disabled
for this
integration untill you upgrade Home Assistant.
</p>
</div>
</div>
@@ -0,0 +1,77 @@
<!-- Used for both overview and store-->
{% extends "base.html" %}
{% block content %}
{% if location == 'store' %}
<div class='hacs-overview-container'>
<input type="text" id="Search" onkeyup="Search()" placeholder="Please enter a search term.." title="Type in a name"
autofocus style="color: var(--primary-text-color)">
</div>
{% if hacs.configuration.frontend_mode == "Grid" %}
{% include 'grid_new.html' with context %}
{% else %}
{% include 'table_new.html' with context %}
{% endif %}
{% endif %}
{% for type in hacs.common.categories %}
{% if type == 'appdaemon' %}
{% set displaytype = 'APPDAEMON APPS' %}
{% elif type == 'python_script' %}
{% set displaytype = 'PYTHON SCRIPTS' %}
{% else %}
{% set displaytype = (type + 's') | upper %}
{% endif %}
{% set repositories = namespace(repositories=[]) %}
{% for repository in hacs.sorted_by_name %}
{% set repo = namespace(include=True) %}
{% if repository.status.hide and repo.include %}
{% set repo.include = False %}
{% endif %}
{% if repository.information.category != type and repo.include %}
{% set repo.include = False %}
{% endif %}
{% if location == 'overview' %}
{% if not repository.status.installed and repo.include %}
{% set repo.include = False %}
{% if repository.information.uid == "172733314" %}
{% set repo.include = True %}
{% endif %}
{% endif %}
{% else %}
{% if repository.information.uid == "172733314" %}
{% set repo.include = False %}
{% endif %}
{% if hacs.configuration.country and repo.include %}
{% if repository.repository_manifest %}
{% if repository.repository_manifest.country %}
{% if hacs.configuration.country not in repository.repository_manifest.country %}
{% set repo.include = False %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% if repo.include %}
{% set repositories.repositories = repositories.repositories+[repository] %}
{% endif %}
{% endfor %}
{% if repositories.repositories %}
{% if hacs.configuration.frontend_mode == "Grid" %}
{% include 'grid.html' with context %}
{% else %}
{% include 'table.html' with context %}
{% endif %}
{% endif %}
{% endfor %}
{% endblock %}
@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block content %}
{% if repository.pending_restart %}
{% set message = "You need to restart (and potentially reconfigure) Home Assistant, for your last operation to be loaded." %}
{% endif %}
{% if repository.information.category == "integration" and repository.status.first_install and repository.status.installed and repository.config_flow %}
{% set message = "
<b>Good news!</b></br>
This integration supports config flows!</br>
This means that you can go to 'Configuration' --> 'Integrations' --> '+' to configure it.</br>
The best part if you do that, is that you do <b>not</b> need to restart Home Assistant.</br></br>
<i>You may need to clear the cache on the integrations page before it shows in the list (+).</i>"%}
{% endif %}
{% if message %}
{% include 'message.html' with context %}
{% endif %}
<div class='hacs-overview-container'>
<div class="row">
<div class="col s12">
<div class="card hacscolor">
<div class="card-content">
<span class="card-title">
<b>{{repository.display_name}}</b>
<a class='dropdown-trigger btn right' href='#' data-target='dropdown1'
style="background-color: var(--primary-color); padding-top: 8px; height: 48px">
<i class="fas fa-bars"></i>
</a>
{% include 'repository/menu.html' with context %}
</span>
<p>{{repository.information.description}}</p></br>
<table class="hacs-table">
{% if repository.status.installed %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Installed {{repository.display_version_or_commit}}:</b>
</td>
<td class="repository text">
{{repository.display_installed_version}}
</td>
{% endif %}
</tr>
<tr class="hacs-table-row repository">
{% if repository.releases.published_tags %}
{% include 'repository/versionselect.html' with context %}
{% else %}
<td class="repository">
<b>Available {{repository.display_version_or_commit}}:</b>
</td>
<td class="repository text">
{{repository.display_available_version}}
</td>
{% endif %}
</tr>
{% if repository.status.selected_tag == repository.information.default_branch %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Last updated:</b>
</td>
<td class="repository text">
{{repository.information.last_updated}}
</td>
</tr>
{% endif %}
{% include 'repository/authors.html' with context %}
</table>
</div>
{% include 'repository/buttons.html' with context %}
</div>
</div>
</div>
</div>
<div class='hacs-overview-container'>
<div class="row">
<div class="col s12">
<div class="card hacscolor">
<div class="card-content" style="margin-top: -20px;">
{{ repository.information.additional_info }}
{% include 'repository/note.html' with context %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,27 @@
{% if repository.information.authors %}
{% if repository.information.category == "integration" %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Author(s):</b>
</td>
<td class="repository text">
{% for author in repository.information.authors %}
{% if "@" in author %}
{% set author = author.split("@")[-1] %}
{% endif %}
<a rel='noreferrer' href='https://github.com/{{author}}' target='_blank'
style='color: var(--primary-color) !important; margin: 2'> @{{author}}</a>
{% endfor %}
</td>
</tr>
{% else %}
<tr class="hacs-table-row repository">
<td class="repository">
<b>Author:</b>
</td>
<td class="repository text">
{{repository.information.authors}}
</td>
</tr>
{% endif %}
{% endif %}
@@ -0,0 +1,50 @@
{% if not repository.can_install %}
{% include 'modal/wrong_ha_version.html' with context %}
{% endif %}
<div class="card-action">
{% if repository.can_install %}
<form action="/hacsapi/{{ hacs.token }}/repository_install" name="install" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a class="actionlink" href="#" onclick="toggleLoading();document.getElementsByName('install')[0].submit()">
{{repository.main_action}}
</a>
</form>
{% else %}
<a class="modal-trigger actionlink" href="#haversion">
{{repository.main_action}}
</a>
{% endif %}
{% if repository.pending_upgrade and repository.display_version_or_commit == "version" %}
<a class="actionlink" rel='noreferrer'
href='https://github.com/{{repository.information.full_name}}/releases/{{repository.available_version}}'
target='_blank'>
CHANGELOG
</a>
{% endif %}
<a class="actionlink" rel='noreferrer' href='https://github.com/{{repository.information.full_name}}' target='_blank'>
repository
</a>
{% if repository.status.installed and repository.information.category == "plugin" %}
{% set name = repository.information.full_name.split("/")[-1] %}
<a class="actionlink" href='/community_plugin/{{name}}/{{repository.information.file_name}}' target='_blank'>
OPEN PLUGIN
</a>
{% endif %}
{% if repository.status.installed and repository.information.uid != "172733314" %}
<form action="/hacsapi/{{ hacs.token }}/repository_uninstall" name="uninstall" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a class="actionlink uninstall" href='#'
onclick="toggleLoading();document.getElementsByName('uninstall')[0].submit()">
UNINSTALL
</a>
</form>
{% endif %}
</div>
@@ -0,0 +1,61 @@
<ul id='dropdown1' class='dropdown-content'>
<form action="/hacsapi/{{ hacs.token }}/repository_update" name="update" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<li>
<a class="dropdown-list-item" href='#' onclick="toggleLoading();document.getElementsByName('update')[0].submit()">
Reload
</a>
</li>
</form>
{% if repository.display_version_or_commit == "version" %}
{% set hide_show = "hide" if repository.status.show_beta else "show" %}
<form action="/hacsapi/{{ hacs.token }}/repository_beta_{{hide_show}}" name="beta" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<li>
<a class="dropdown-list-item" href='#' onclick="toggleLoading();document.getElementsByName('beta')[0].submit()">
{{hide_show | title}} beta
</a>
</li>
</form>
{% endif %}
{% if not repository.status.installed %}
{% if not repository.custom %}
{% if repository.information.uid != "172733314" %}
<form action="/hacsapi/{{ hacs.token }}/repository_hide" name="repository_hide" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<li>
<a class="dropdown-list-item" href='#'
onclick="toggleLoading();document.getElementsByName('repository_hide')[0].submit()">
Hide
</a>
</li>
</form>
{% endif %}
{% endif %}
{% endif %}
<li>
<a class="dropdown-list-item" rel='noreferrer'
href="https://github.com/{{repository.information.full_name}}/issues/" target="_blank">
Open issue
</a>
</li>
{% if repository.information.uid != "172733314" %}
<li>
<a class="dropdown-list-item" rel='noreferrer'
href="https://github.com/hacs/integration/issues/new?title={{repository.information.full_name}}&labels=flag&assignee=ludeeus&template=flag.md"
target="_blank">
Flag this
</a>
</li>
{% endif %}
</ul>
@@ -0,0 +1,42 @@
{% if repository.additional_info %}
</br>
<hr></br>
{% endif %}
{% if repository.information.category == "appdaemon" %}
<i>
When installed, this will be located in '{{repository.content.path.local}}',
you still need to add it to your 'apps.yaml' file.
</i>
{% elif repository.information.category == "integration" %}
<i>
When installed, this will be located in '{{repository.content.path.local}}',
you still need to add it to your 'configuration.yaml' file.
</i>
{% elif repository.information.category == "plugin" %}
{% set name = repository.information.full_name.split("/")[-1] %}
<i>
When installed, this will be located in '{{repository.content.path.local }}',
you still need to add it to your lovelace configuration ('ui-lovelace.yaml' or the raw UI config editor).
</i>
</br></br>
<i>
When you add this to your configuration use this:
</i></br>
<pre id="LovelaceExample" class="yaml">
- url: /community_plugin/{{name}}/{{repository.information.file_name}}
{%- if repository.information.javascript_type %}
type: {{repository.information.javascript_type}}
{%- else %}
type: Could not determine the type of this plugin, check the repository.
{%- endif -%}
</pre>
<a title="Copy content to clipboard" id="lovelacecopy" onclick="CopyToLovelaceExampleToClipboard()">
<i class="fa fa-copy"></i>
</a>
{% endif %}
@@ -0,0 +1,24 @@
{% set tags = repository.releases.published_tags | sort(True) %}
{% set tags = tags + [repository.information.default_branch] %}
{% if repository.status.selected_tag %}
{% set selected_tag = repository.status.selected_tag %}
{% else %}
{% set selected_tag = repository.versions.available %}
{% endif %}
<td class="repository">
<b>Available versions:</b>
</td>
<td class="repository">
<form action="/hacsapi/{{ hacs.token }}/repository_select_tag" name="selected_tag" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<select name="selected_tag" class="hacs-select repository"
onchange="toggleLoading();document.getElementsByName('selected_tag')[0].submit()">
{% for tag in tags %}
<option {{'selected' if selected_tag == tag else ''}} class='hacscolor' value='{{tag}}'>{{tag}}</option>
{% endfor %}
</select>
</form>
</td>
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
{% if hacs.configuration.dev %}
{% set message = "You have 'dev' enabled for HACS, this is not intended for regular use, no support will be given if you break something." %}
{% include 'message.html' with context %}
{% endif %}
{% include 'modal/upgrade_all.html' with context %}
{% include 'settings/hacs_info.html' with context %}
{% include 'settings/buttons.html' with context %}
{% include 'settings/custom_repositories.html' with context %}
{% include 'settings/hidden_repositories.html' with context %}
{% if hacs.configuration.dev and not hacs.system.status.background_task %}
{% include 'settings/developer.html' with context %}
{% endif %}
{% endblock %}
@@ -0,0 +1,26 @@
<div class='hacs-overview-container minigrid'>
<form action="/hacsapi/{{ hacs.token }}/repositories_reload" name="reload" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
onclick="toggleLoading();document.getElementsByName('reload')[0].submit()">
RELOAD DATA
</a>
</form>
<a href="#modal_upgrade_all" class="waves-effect waves-light btn modal-trigger hacsbutton"
style="background-color: var(--google-red-500) !important; font-weight: bold;">
UPGRADE ALL
</a>
<a href='https://github.com/hacs/integration' rel='noreferrer' class='waves-effect waves-light btn hacsbutton'
target="_blank">
HACS REPO
</a>
<a href='https://github.com/hacs/integration/issues' rel='noreferrer'
class='waves-effect waves-light btn hacsbutton' target="_blank">
OPEN ISSUE
</a>
</div>
@@ -0,0 +1,62 @@
<div class='hacs-overview-container'>
<div class="row">
<ul class="collection with-header hacslist">
<li class="collection-header hacscolor hacslist">
<h5>CUSTOM REPOSITORIES</h5>
</li>
{% if hacs.system.status.background_task %}
<li class="collection-item hacscolor hacslist">
Loading custom repositories...
</li>
{% else %}
{% for repository in hacs.sorted_by_repository_name %}
{% if repository.custom %}
<li class="collection-item hacscolor hacslist">
<div>
<a href="/hacsweb/{{ hacs.token }}/repository/{{repository.information.uid}}">
<span class="hacs-badge">
{{repository.information.category | upper}}
</span>
{{repository.information.full_name }}
</a>
{% if repository.status.installed %}
<i title="Remove is not possible when installed."
class="secondary-content fas fa-trash-alt disabledaction"></i>
{% else %}
<form action="/hacsapi/{{ hacs.token }}/repository_remove" name="remove" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a class="secondary-content" href="#"
onclick="toggleLoading();document.getElementsByName('remove')[0].submit()"
style=" color: var(--primary-color)">
<i title="Remove." class="fas fa-trash-alt"></i>
</a>
</form>
{% endif %}
</div>
</li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
{% if not hacs.system.status.background_task %}
<form class="minigrid" action="/hacsapi/{{ hacs.token }}/repository_register" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded">
<input class="custom_repository" id="custom_url" type="text" name="custom_url"
placeholder=" ADD CUSTOM REPOSITORY" style="width: 70%; color: var(--primary-text-color)">
<select name="repository_type" class="hacs-select custom_repository">
<option disabled selected value>Category</option>
{% for element_type in hacs.common.categories | sort %}
<option class='hacscolor' value='{{element_type}}'>{{element_type | title}}</option>
{% endfor %}
</select>
<button class="btn waves-effect waves-light right" type="submit" name="add" onclick="toggleLoading()"
style="background-color: var(--primary-color); height: 44px;">
<i class="fas fa-save"></i>
</button>
</form>
{% endif %}
</div>
</div>
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/remove_new_flag" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
REMOVE FLAGS
</button>
</form>
</div>
</div>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
{% for repository in hacs.store.frontend|sort(attribute='name') %}
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/devrepository" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="repository" value="{{repository.information.uid}}">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
{{ repository.name }}
</button>
</form>
{% endfor %}
{% endblock %}
@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/set_ha_version_action" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input class="hacscolor" type="text" name="ha_version" value="{{hacs.store.ha_version}}">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
SET
</button>
</form>
</div>
</div>
{% endblock %}
@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/dev_template" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input class="hacscolor" type="text" name="repository_id" value="{{ hacs.dev_template_id }}">
<textarea class="hacscolor" name="template" style="width:100%;height:250px;"></textarea></br>
<button name="set" class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
TEST
</button>
<button name="clear" class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
CLEAR
</button>
</form>
</div>
</div>
{% if hacs.dev_template %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
{{ hacs.dev_template }}
</div>
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<label>{{hacs.token}}</label>
</div>
</div>
{% endblock %}
@@ -0,0 +1,19 @@
<div class="hacs-overview-container hacs-color minigrid" style="margin-top: 30px;">
<div class="hacs-card-standalone">
<h5 style="margin-left: 5px;">DEVELOPER TOOLS</h5>
{% for button in [
"remove_new_flag",
"repositories",
"set_ha_version",
"token",
"template_test"
] %}
<form style="display: contents" action="/hacsapi/{{ hacs.token }}/devview" method="post" accept-charset="utf-8" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="view" value="{{button}}">
<button class="waves-effect waves-light btn hacsadminbutton" type="submit" onclick="toggleLoading()">
{{ button.replace("_"," ")}}
</button>
</form>
{% endfor %}
</div>
</div>
@@ -0,0 +1,38 @@
{% if hacs.get_by_id("172733314").pending_restart %}
{% set pending_restart = " <b>(RESTART PENDING!)</b>" %}
{% else %}
{% set pending_restart = "" %}
{% endif %}
<div class='hacs-overview-container'>
<div class="hacs-card-standalone">
<h5>HACS (Home Assistant Community Store)</h5>
<table class="hacs-table">
<tr class="hacs-table-row repository">
<td class="repository">
<b>Version:</b>
</td>
<td class="repository text">
{{hacs.version}}{{pending_restart}}
</td>
</tr>
<tr class="hacs-table-row repository">
<td class="repository">
<b>Display:</b>
</td>
<td class="repository">
<form action="/hacsapi/{{ hacs.token }}/frontend_mode" name="overview_display" method="post"
accept-charset="utf-8" enctype="application/x-www-form-urlencoded" class="hacs-form">
<select name="view_type" class="hacs-select repository"
onchange="toggleLoading();document.getElementsByName('overview_display')[0].submit()">
{% for displayoption in ["Grid", "Table"] %}
<option {{"selected" if hacs.configuration.frontend_mode == displayoption else ""}} class="hacscolor"
value="{{displayoption}}">{{displayoption}}</option>
{% endfor %}
</select>
</form>
</td>
</tr>
</table>
</div>
</div>
@@ -0,0 +1,42 @@
{% set repositories = namespace(hidden=False) %}
{% for repository in hacs.repositories_list_repo %}
{% if repository.hide %}
{% set repositories.hidden = True %}
{% endif %}
{% endfor %}
{% if repositories.hidden %}
<div class='hacs-overview-container'>
<div class="row">
<ul class="collection with-header hacslist">
<li class="collection-header hacscolor hacslist">
<h5>HIDDEN REPOSITORIES</h5>
</li>
{% if hacs.system.status.background_task %}
<li class="collection-item hacscolor hacslist">
Loading hidden repositories...
</li>
{% else %}
{% for repository in hacs.repositories_list_repo %}
{% if repository.hide %}
<li class="collection-item hacscolor hacslist">
<div>
<form action="/hacsapi/{{ hacs.token }}/repository_unhide" name="remove" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="repository_id" value="{{repository.information.uid}}">
<a href="#" onclick="toggleLoading();document.getElementsByName('remove')[0].submit()"
style=" color: var(--primary-color)">
<i title="Unhide" class="fas fa-plus-circle" style="padding-right: 8px"></i>
</a>
{{repository.repository_name}}
<span class="hacs-badge">{{repository.repository_type}}</span>
</form>
</div>
</li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
</div>
</div>
{% endif %}
@@ -0,0 +1,42 @@
<div class='hacs-overview-container'>
<div class="row">
<h5>{{ displaytype }}</h5>
<table class="hacs-table">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th class="smal-hide">Description</th>
{% if location == 'overview' %}
<th class="smal-hide">Installed</th>
<th class="smal-hide">Available</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for repository in repositories.repositories %}
<tr class="hacs-table-row"
onclick="toggleLoading();window.location='/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}';">
<input type="hidden" name="author" value="{{repository.information.authors}}">
<input type="hidden" name="topics" value="{{repository.information.topics}}">
<input type="hidden" name="full_name" value="{{repository.information.full_name}}">
<td title="{{ repository.display_status_description }}">
{% if location == 'store' and repository.status.new %}
<span style='margin-left: 25%' class='repository-list-badge hacs-badge'>NEW</span>
{% else %}
<i style='margin-left: 25%' class='fas fa-cube card-status {{repository.display_status}}'></i></td>
{% endif %}
<td>
{{repository.display_name}}
</td>
<td class="hacs-card-content smal-hide">{{repository.information.description}}</td>
{% if location == 'overview' %}
<td class="smal-hide">{{repository.display_installed_version}}</td>
<td class="smal-hide">{{repository.display_available_version}}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,44 @@
{% set repositories = namespace(repositories=[]) %}
{% for repository in hacs.sorted_by_name %}
{% if repository.status.new %}
{% set repositories.repositories = repositories.repositories+[repository] %}
{% endif %}
{% endfor %}
{% if repositories.repositories %}
<div class='hacs-overview-container'>
<div class="row">
<h5>NEW REPOSITORIES (ALL)</h5>
<form action="/hacsapi/{{ hacs.token }}/remove_new_flag" name="remove_new_flag" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded" class="hacs-form">
<input type="hidden" name="return" value="store">
<a class="waves-effect waves-light btn modal-trigger hacsbutton" href="#"
onclick="toggleLoading();document.getElementsByName('remove_new_flag')[0].submit()" style="width: 95%;">
Remove the NEW flag for all new items
</a>
</form>
<table class="hacs-table">
<thead>
<tr>
<th>Category</th>
<th>Name</th>
<th class="smal-hide">Description</th>
</tr>
</thead>
<tbody>
{% for repository in repositories.repositories %}
<tr class="hacs-table-row"
onclick="toggleLoading();window.location='/hacsweb/{{ hacs.token }}/repository/{{ repository.information.uid }}?timestamp={{ timestamp }}';">
<td>
<span class="hacs-badge">{{repository.information.category | upper}}</span>
</td>
<td>
{{repository.display_name}}
</td>
<td class="hacs-card-content smal-hide">{{repository.information.description}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
@@ -0,0 +1,391 @@
"""Initialize the HACS base."""
# pylint: disable=unused-argument, bad-continuation
import json
import uuid
from datetime import timedelta
from homeassistant.helpers.event import async_call_later, async_track_time_interval
from aiogithubapi import AIOGitHubException, AIOGitHubRatelimit
from integrationhelper import Logger
from ..const import ELEMENT_TYPES
from ..store import async_load_from_store, async_save_to_store
class HacsStatus:
"""HacsStatus."""
startup = False
background_task = False
reloading_data = False
upgrading_all = False
class HacsCommon:
"""Common for HACS."""
categories = []
blacklist = []
default = []
installed = []
skip = []
class System:
"""System info."""
status = HacsStatus()
config_path = None
ha_version = None
disabled = False
class Developer:
"""Developer settings/tools."""
template_id = "Repository ID"
template_content = ""
template_raw = ""
@property
def devcontainer(self):
"""Is it a devcontainer?"""
import os
if "DEVCONTAINER" in os.environ:
return True
return False
class Hacs:
"""The base class of HACS, nested thoughout the project."""
token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}"
hacsweb = f"/hacsweb/{token}"
hacsapi = f"/hacsapi/{token}"
repositories = []
repo = None
data_repo = None
developer = Developer()
data = None
configuration = None
logger = Logger("hacs")
github = None
hass = None
version = None
system = System()
tasks = []
common = HacsCommon()
@staticmethod
def init(hass, github_token):
"""Return a initialized HACS object."""
return Hacs()
def get_by_id(self, repository_id):
"""Get repository by ID."""
try:
for repository in self.repositories:
if repository.information.uid == repository_id:
return repository
except Exception: # pylint: disable=broad-except
pass
return None
def get_by_name(self, repository_full_name):
"""Get repository by full_name."""
try:
for repository in self.repositories:
if repository.information.full_name == repository_full_name:
return repository
except Exception: # pylint: disable=broad-except
pass
return None
def is_known(self, repository_full_name):
"""Return a bool if the repository is known."""
for repository in self.repositories:
if repository.information.full_name == repository_full_name:
return True
return False
@property
def sorted_by_name(self):
"""Return a sorted(by name) list of repository objects."""
return sorted(self.repositories, key=lambda x: x.display_name)
@property
def sorted_by_repository_name(self):
"""Return a sorted(by repository_name) list of repository objects."""
return sorted(self.repositories, key=lambda x: x.information.full_name)
async def register_repository(self, full_name, category, check=True):
"""Register a repository."""
from ..repositories.repository import RERPOSITORY_CLASSES
if full_name in self.common.skip:
if full_name != "hacs/integration":
self.logger.debug(f"Skipping {full_name}")
return
if category not in RERPOSITORY_CLASSES:
self.logger.error(f"{category} is not a valid repository category.")
return False
repository = RERPOSITORY_CLASSES[category](full_name)
if check:
try:
await repository.registration()
if repository.validate.errors:
self.common.skip.append(repository.information.full_name)
if not self.system.status.startup:
self.logger.error(f"Validation for {full_name} failed.")
return repository.validate.errors
repository.logger.info("Registration complete")
except AIOGitHubException as exception:
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
self.common.skip.append(repository.information.full_name)
if not self.system.status.startup:
self.logger.error(
f"Validation for {full_name} failed with {exception}."
)
return exception
self.hass.bus.async_fire(
"hacs/repository",
{
"id": 1337,
"action": "registration",
"repository": repository.information.full_name,
"repository_id": repository.information.uid,
},
)
self.repositories.append(repository)
async def startup_tasks(self):
"""Tasks tha are started after startup."""
self.system.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
await self.handle_critical_repositories_startup()
await self.handle_critical_repositories()
await self.load_known_repositories()
await self.clear_out_blacklisted_repositories()
self.tasks.append(
async_track_time_interval(
self.hass, self.recuring_tasks_installed, timedelta(minutes=30)
)
)
self.tasks.append(
async_track_time_interval(
self.hass, self.recuring_tasks_all, timedelta(minutes=800)
)
)
self.system.status.startup = False
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
async def handle_critical_repositories_startup(self):
"""Handled critical repositories during startup."""
alert = False
critical = await async_load_from_store(self.hass, "critical")
if not critical:
return
for repo in critical:
if not repo["acknowledged"]:
alert = True
if alert:
self.logger.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create(
title="URGENT!", message="**Check the HACS panel!**"
)
async def handle_critical_repositories(self):
"""Handled critical repositories during runtime."""
# Get critical repositories
instored = []
critical = []
was_installed = False
try:
critical = await self.data_repo.get_contents("critical")
critical = json.loads(critical.content)
except AIOGitHubException:
pass
if not critical:
self.logger.debug("No critical repositories")
return
stored_critical = await async_load_from_store(self.hass, "critical")
for stored in stored_critical or []:
instored.append(stored["repository"])
stored_critical = []
for repository in critical:
self.common.blacklist.append(repository["repository"])
repo = self.get_by_name(repository["repository"])
stored = {
"repository": repository["repository"],
"reason": repository["reason"],
"link": repository["link"],
"acknowledged": True,
}
if repository["repository"] not in instored:
if repo is not None and repo.installed:
self.logger.critical(
f"Removing repository {repository['repository']}, it is marked as critical"
)
was_installed = True
stored["acknowledged"] = False
# Uninstall from HACS
repo.remove()
await repo.uninstall()
stored_critical.append(stored)
# Save to FS
await async_save_to_store(self.hass, "critical", stored_critical)
# Resart HASS
if was_installed:
self.logger.critical("Resarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))
async def recuring_tasks_installed(self, notarealarg=None):
"""Recuring tasks for installed repositories."""
self.logger.debug(
"Starting recuring background task for installed repositories"
)
self.system.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
for repository in self.repositories:
if repository.status.installed:
try:
await repository.update_repository()
repository.logger.debug("Information update done.")
except AIOGitHubException:
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
self.logger.debug(
"Recuring background task for installed repositories done"
)
return
await self.handle_critical_repositories()
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
self.logger.debug("Recuring background task for installed repositories done")
async def recuring_tasks_all(self, notarealarg=None):
"""Recuring tasks for all repositories."""
self.logger.debug("Starting recuring background task for all repositories")
self.system.status.background_task = True
self.hass.bus.async_fire("hacs/status", {})
self.logger.debug(self.github.ratelimits.remaining)
self.logger.debug(self.github.ratelimits.reset_utc)
for repository in self.repositories:
try:
await repository.update_repository()
repository.logger.debug("Information update done.")
except AIOGitHubException:
self.system.status.background_task = False
self.hass.bus.async_fire("hacs/status", {})
await self.data.async_write()
self.logger.debug("Recuring background task for all repositories done")
return
await self.load_known_repositories()
await self.clear_out_blacklisted_repositories()
self.system.status.background_task = False
await self.data.async_write()
self.hass.bus.async_fire("hacs/status", {})
self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
self.logger.debug("Recuring background task for all repositories done")
async def clear_out_blacklisted_repositories(self):
"""Clear out blaclisted repositories."""
need_to_save = False
for repository in self.common.blacklist:
if self.is_known(repository):
repository = self.get_by_name(repository)
if repository.status.installed:
self.logger.error(
f"You have {repository.information.full_name} installed with HACS, this repositroy has been blacklisted, please consider removing it."
)
else:
need_to_save = True
repository.remove()
if need_to_save:
await self.data.async_write()
async def get_repositories(self):
"""Return a list of repositories."""
repositories = {}
if self.configuration.dev:
if self.developer.devcontainer:
repositories = {
"appdaemon": ["ludeeus/ad-hacs"],
"integration": ["ludeeus/integration-hacs"],
"plugin": ["maykar/compact-custom-header"],
"python_script": ["ludeeus/ps-hacs"],
"theme": ["ludeeus/theme-hacs"],
}
else:
for category in self.common.categories:
remote = await self.data_repo.get_contents(category)
repositories[category] = json.loads(remote.content)
if category == "plugin":
org = await self.github.get_org_repos("custom-cards")
for repo in org:
repositories[category].append(repo.full_name)
if category == "integration":
org = await self.github.get_org_repos("custom-components")
for repo in org:
repositories[category].append(repo.full_name)
if category == "theme":
org = await self.github.get_org_repos(
"home-assistant-community-themes"
)
for repo in org:
repositories[category].append(repo.full_name)
for category in repositories:
for repo in repositories[category]:
if repo not in self.common.default:
self.common.default.append(repo)
return repositories
async def load_known_repositories(self):
"""Load known repositories."""
self.logger.info("Loading known repositories")
blacklist = await self.data_repo.get_contents("blacklist")
repositories = await self.get_repositories()
for item in json.loads(blacklist.content):
if item not in self.common.blacklist:
self.common.blacklist.append(item)
for category in repositories:
for repo in repositories[category]:
if repo in self.common.blacklist:
continue
if self.is_known(repo):
continue
try:
await self.register_repository(repo, category)
except (AIOGitHubException, AIOGitHubRatelimit):
pass
@@ -0,0 +1,72 @@
"""Backup."""
import os
import shutil
import tempfile
from time import sleep
from integrationhelper import Logger
BACKUP_PATH = tempfile.gettempdir() + "/hacs_backup/"
class Backup:
"""Backup."""
def __init__(self, local_path, backup_path=BACKUP_PATH):
"""initialize."""
self.logger = Logger("hacs.backup")
self.local_path = local_path
self.backup_path = backup_path
self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}"
def create(self):
"""Create a backup in /tmp"""
if not os.path.exists(self.local_path):
return
if os.path.exists(self.backup_path):
shutil.rmtree(self.backup_path)
while os.path.exists(self.backup_path):
sleep(0.1)
os.makedirs(self.backup_path, exist_ok=True)
try:
if os.path.isfile(self.local_path):
shutil.copyfile(self.local_path, self.backup_path_full)
os.remove(self.local_path)
else:
shutil.copytree(self.local_path, self.backup_path_full)
shutil.rmtree(self.local_path)
while os.path.exists(self.local_path):
sleep(0.1)
self.logger.debug(
f"Backup for {self.local_path}, created in {self.backup_path_full}"
)
except Exception: # pylint: disable=broad-except
pass
def restore(self):
"""Restore from backup."""
if not os.path.exists(self.backup_path_full):
return
if os.path.isfile(self.backup_path_full):
if os.path.exists(self.local_path):
os.remove(self.local_path)
shutil.copyfile(self.backup_path_full, self.local_path)
else:
if os.path.exists(self.local_path):
shutil.rmtree(self.local_path)
while os.path.exists(self.local_path):
sleep(0.1)
shutil.copytree(self.backup_path_full, self.local_path)
self.logger.debug(
f"Restored {self.local_path}, from backup {self.backup_path_full}"
)
def cleanup(self):
"""Cleanup backup files."""
if os.path.exists(self.backup_path):
shutil.rmtree(self.backup_path)
while os.path.exists(self.backup_path):
sleep(0.1)
self.logger.debug(f"Backup dir {self.backup_path} cleared")
@@ -0,0 +1,55 @@
"""HACS Configuration."""
import attr
from custom_components.hacs.hacsbase.exceptions import HacsUserScrewupException
@attr.s(auto_attribs=True)
class Configuration:
"""Configuration class."""
# Main configuration:
appdaemon_path: str = "appdaemon/apps/"
appdaemon: bool = False
config: dict = {}
config_entry: dict = {}
config_type: str = None
dev: bool = False
frontend_mode: str = "Grid"
options: dict = {}
plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/"
python_script: bool = False
sidepanel_icon: str = "mdi:alpha-c-box"
sidepanel_title: str = "Community"
theme_path: str = "themes/"
theme: bool = False
token: str = None
# Config options:
country: str = "ALL"
experimental: bool = False
release_limit: int = 5
@staticmethod
def from_dict(configuration: dict, options: dict):
"""Set attributes from dicts."""
if isinstance(options, bool) or isinstance(configuration.get("options"), bool):
raise HacsUserScrewupException("Configuration is not valid.")
if options is None:
options = {}
if not configuration:
raise HacsUserScrewupException("Configuration is not valid.")
config = Configuration()
config.config = configuration
config.options = options
for conf_type in [configuration, options]:
for key in conf_type:
setattr(config, key, conf_type[key])
return config
@@ -0,0 +1,10 @@
"""Constants for HACS"""
# pylint: disable=unused-import
STORAGE_VERSION = "6"
STORENAME = "hacs"
# Messages
NOT_SUPPORTED_HA_VERSION = "You have version '{}' of Home Assistant, but version '{}' of '{}' require version '{}' of Home Assistant, install and upgrades are disabled for this integration untill you upgrade Home Assistant."
NO_ELEMENTS = "No elements to show, open the store to install some awesome stuff."
@@ -0,0 +1,215 @@
"""Data handler for HACS."""
import os
import json
from integrationhelper import Logger
from . import Hacs
from .const import STORAGE_VERSION
from ..const import VERSION
from ..repositories.manifest import HacsManifest
from ..store import async_save_to_store, async_load_from_store
STORES = {
"old": "hacs",
"hacs": "hacs.hacs",
"installed": "hacs.installed",
"repositories": "hacs.repositories",
}
class HacsData(Hacs):
"""HacsData class."""
def __init__(self):
"""Initialize."""
self.logger = Logger("hacs.data")
def check_corrupted_files(self):
"""Return True if one (or more) of the files are corrupted."""
for store in STORES:
path = f"{self.system.config_path}/.storage/{STORES[store]}"
if os.path.exists(path):
if os.stat(path).st_size == 0:
# File is empty (corrupted)
return True
return False
def read(self, store):
"""Return data from a store."""
path = f"{self.system.config_path}/.storage/{STORES[store]}"
content = None
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as storefile:
content = storefile.read()
content = json.loads(content)
return content
async def async_write(self):
"""Write content to the store files."""
if self.system.status.background_task:
return
self.logger.debug("Saving data")
# Hacs
await async_save_to_store(
self.hass, "hacs", {"view": self.configuration.frontend_mode}
)
# Repositories
content = {}
for repository in self.repositories:
if repository.repository_manifest is not None:
repository_manifest = repository.repository_manifest.manifest
else:
repository_manifest = None
content[repository.information.uid] = {
"authors": repository.information.authors,
"topics": repository.information.topics,
"category": repository.information.category,
"description": repository.information.description,
"full_name": repository.information.full_name,
"hide": repository.status.hide,
"installed_commit": repository.versions.installed_commit,
"installed": repository.status.installed,
"last_commit": repository.versions.available_commit,
"last_release_tag": repository.versions.available,
"repository_manifest": repository_manifest,
"name": repository.information.name,
"new": repository.status.new,
"selected_tag": repository.status.selected_tag,
"show_beta": repository.status.show_beta,
"version_installed": repository.versions.installed,
}
await async_save_to_store(self.hass, "repositories", content)
self.hass.bus.async_fire("hacs/repository", {})
self.hass.bus.fire("hacs/config", {})
async def restore(self):
"""Restore saved data."""
hacs = {}
repositories = {}
try:
hacs = await async_load_from_store(self.hass, "hacs")
except KeyError:
await async_save_to_store(self.hass, "hacs", self.data.read("hacs")["data"])
hacs = await async_load_from_store(self.hass, "hacs")
try:
repositories = await async_load_from_store(self.hass, "repositories")
except KeyError:
await async_save_to_store(
self.hass, "repositories", self.data.read("repositories")["data"]
)
repositories = await async_load_from_store(self.hass, "repositories")
try:
if self.check_corrupted_files():
# Coruptted installation
self.logger.critical("Restore failed one or more files are corrupted!")
return False
if hacs is None and repositories is None:
# Assume new install
return True
self.logger.info("Restore started")
# Hacs
self.configuration.frontend_mode = hacs.get("view", "Grid")
# Repositories
repositories = repositories
for entry in repositories:
repo = repositories[entry]
if repo["full_name"] == "custom-components/hacs":
# Skip the old repo location
continue
if not self.is_known(repo["full_name"]):
await self.register_repository(
repo["full_name"], repo["category"], False
)
repository = self.get_by_name(repo["full_name"])
if repository is None:
self.logger.error(f"Did not find {repo['full_name']}")
continue
# Restore repository attributes
if repo.get("authors") is not None:
repository.information.authors = repo["authors"]
if repo.get("topics", []):
repository.information.topics = repo["topics"]
if repo.get("description") is not None:
repository.information.description = repo["description"]
if repo.get("name") is not None:
repository.information.name = repo["name"]
if repo.get("hide") is not None:
repository.status.hide = repo["hide"]
if repo.get("installed") is not None:
repository.status.installed = repo["installed"]
if repository.status.installed:
repository.status.first_install = False
if repo.get("selected_tag") is not None:
repository.status.selected_tag = repo["selected_tag"]
if repo.get("repository_manifest") is not None:
repository.repository_manifest = HacsManifest.from_dict(
repo["repository_manifest"]
)
if repo.get("show_beta") is not None:
repository.status.show_beta = repo["show_beta"]
if repo.get("last_commit") is not None:
repository.versions.available_commit = repo["last_commit"]
repository.information.uid = entry
if repo.get("last_release_tag") is not None:
repository.releases.last_release = repo["last_release_tag"]
repository.versions.available = repo["last_release_tag"]
if repo.get("new") is not None:
repository.status.new = repo["new"]
if repo["full_name"] == "hacs/integration":
repository.versions.installed = VERSION
repository.status.installed = True
if "b" in VERSION:
repository.status.show_beta = True
elif repo.get("version_installed") is not None:
repository.versions.installed = repo["version_installed"]
if repo.get("installed_commit") is not None:
repository.versions.installed_commit = repo["installed_commit"]
self.logger.info("Restore done")
except Exception as exception:
self.logger.critical(
f"[{exception}] Restore Failed! see https://github.com/hacs/integration/issues/639 for more details."
)
return False
return True
def save(logger, path, content):
"""Save file."""
from .backup import Backup
backup = Backup(path)
backup.create()
try:
content = {"data": content, "schema": STORAGE_VERSION}
with open(path, "w", encoding="utf-8") as storefile:
json.dump(content, storefile, indent=4)
except Exception as exception: # pylint: disable=broad-except
logger.warning(f"Saving {path} failed - {exception}")
backup.restore()
backup.cleanup()
@@ -0,0 +1,45 @@
"""Custom Exceptions."""
class HacsBaseException(Exception):
"""Super basic."""
class HacsUserScrewupException(HacsBaseException):
"""Raise this when the user does something they should not do."""
class HacsNotSoBasicException(HacsBaseException):
"""Not that basic."""
class HacsDataFileMissing(HacsBaseException):
"""Raise this storage datafile is missing."""
class HacsDataNotExpected(HacsBaseException):
"""Raise this when data returned from storage is not ok."""
class HacsRepositoryInfo(HacsBaseException):
"""Raise this when repository info is missing/wrong."""
class HacsRequirement(HacsBaseException):
"""Raise this when repository is missing a requirement."""
class HacsMissingManifest(HacsBaseException):
"""Raise this when manifest is missing."""
def __init__(self, message="The manifest file is missing in the repository."):
super().__init__(message)
self.message = message
class HacsBlacklistException(HacsBaseException):
"""Raise this when the repository is currently in the blacklist."""
def __init__(self, message="The repository is currently in the blacklist."):
super().__init__(message)
self.message = message
@@ -0,0 +1,129 @@
"""HACS Migration logic."""
# pylint: disable=broad-except,no-member
from shutil import copy2
from integrationhelper import Logger, Validate
from . import Hacs
from .data import save, STORES
from .const import STORAGE_VERSION
MIGRATIONS = {}
def register(cls):
"""Register steps."""
MIGRATIONS[cls.from_version] = cls
return cls
class ValidateData(Hacs):
"""Validate."""
def validate_local_data_file(self):
"""Validate content."""
validate = Validate()
old_data = self.data.read("old")
if old_data is None:
# new install.
return True
if old_data.get("hacs", {}).get("schema") is None:
return True
if old_data.get("hacs", {}).get("schema") == STORAGE_VERSION:
# Newest version, no need to do anything.
return
current = old_data.get("hacs", {}).get("schema")
for version in range(int(current), int(STORAGE_VERSION)):
if current in MIGRATIONS:
MIGRATIONS[current](old_data).migrate()
else:
validate.errors.append(f"Missing migration step for {version}")
if validate.errors:
for error in validate.errors:
self.logger.critical(error)
return validate.success
class Migration(Hacs):
"""Hacs migrations"""
def __init__(self, old_data=None):
"""initialize migration."""
self.old_data = old_data
self.logger = Logger("hacs.migration")
self.source = f"{self.system.config_path}/.storage/hacs"
self.cleanup()
self.backup()
def cleanup(self):
"""Remove files no longer in use."""
def backup(self):
"""Back up old file."""
destination = f"{self.source}.{self.from_version}"
self.logger.info(f"Backing up current file to '{destination}'")
copy2(self.source, destination)
@register
class FromVersion4(Migration):
"""Migrate from version 4"""
from_version = "4"
def migrate(self):
"""Start migration."""
self.logger.info(f"Starting migration from {self.from_version}")
hacs = self.old_data["hacs"]
repositories = self.old_data["repositories"]
installed = {}
for repository in repositories:
repository = repositories[repository]
repository["full_name"] = repository["repository_name"]
repository["category"] = repository["repository_type"]
if not repository["installed"]:
repository["show_beta"] = False
else:
if repository["version_installed"] is not None:
version_type = "version"
version_installed = repository["version_installed"]
version_available = repository["last_release_tag"]
else:
version_type = "commit"
version_installed = repository["installed_commit"]
version_available = repository["last_commit"]
if repository["full_name"] != "hacs/integration":
installed[repository["repository_name"]] = {
"version_type": str(version_type),
"version_installed": str(version_installed),
"version_available": str(version_available),
}
path = f"{self.system.config_path}/.storage/{STORES['hacs']}"
save(self.logger, path, hacs)
path = f"{self.system.config_path}/.storage/{STORES['repositories']}"
save(self.logger, path, repositories)
path = f"{self.system.config_path}/.storage/{STORES['installed']}"
save(self.logger, path, installed)
self.logger.info("Migration done")
@register
class FromVersion5(Migration):
"""Migrate from version 5"""
from_version = "5"
def migrate(self):
"""Start migration."""
self.logger.info("Migration done")
@@ -0,0 +1,16 @@
"""HACS Startup."""
from . import Hacs
class HacsStartup(Hacs):
"""Startup class."""
async def run_startup(self):
"""Run startup tasks for HACS."""
self.logger.critical("Startup!")
if self.configuration.dev:
self.logger.critical("Running in DEV mode!")
if self.developer.devcontainer:
self.logger.critical("Running inside a devcontainer")
self.logger.critical("Some features have been disabled")
self.logger.critical("It will only fetch one repository pr category")
@@ -0,0 +1,6 @@
"""HACS Updater."""
from . import Hacs
class HacsUpdate(Hacs):
"""Update class."""
@@ -0,0 +1 @@
"""Initialize handlers."""
@@ -0,0 +1,78 @@
"""Download."""
import os
import gzip
import shutil
import aiofiles
import async_timeout
from integrationhelper import Logger
import backoff
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from ..hacsbase.exceptions import HacsNotSoBasicException
@backoff.on_exception(backoff.expo, Exception, max_tries=5)
async def async_download_file(hass, url):
"""
Download files, and return the content.
"""
logger = Logger("hacs.download.downloader")
if url is None:
return
# There is a bug somewhere... TODO: Find that bug....
if "tags/" in url:
url = url.replace("tags/", "")
logger.debug(f"Donwloading {url}")
result = None
with async_timeout.timeout(60, loop=hass.loop):
request = await async_get_clientsession(hass).get(url)
# Make sure that we got a valid result
if request.status == 200:
result = await request.read()
else:
raise HacsNotSoBasicException(
"Got status code {} when trying to download {}".format(
request.status, url
)
)
return result
async def async_save_file(location, content):
"""Save files."""
logger = Logger("hacs.download.save")
logger.debug(f"Saving {location}")
mode = "w"
encoding = "utf-8"
errors = "ignore"
if not isinstance(content, str):
mode = "wb"
encoding = None
errors = None
try:
async with aiofiles.open(
location, mode=mode, encoding=encoding, errors=errors
) as outfile:
await outfile.write(content)
outfile.close()
# Create gz for .js files
if os.path.isfile(location):
if location.endswith(".js") or location.endswith(".css"):
with open(location, "rb") as f_in:
with gzip.open(location + ".gz", "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
except Exception as error: # pylint: disable=broad-except
msg = "Could not write data to {} - {}".format(location, error)
logger.debug(msg)
return os.path.exists(location)
@@ -0,0 +1,29 @@
"""Custom template support."""
# pylint: disable=broad-except
from jinja2 import Template
from integrationhelper import Logger
def render_template(content, context):
"""Render templates in content."""
# Fix None issues
if context.releases.last_release_object is not None:
prerelease = context.releases.last_release_object.prerelease
else:
prerelease = False
# Render the template
try:
render = Template(content)
render = render.render(
installed=context.status.installed,
pending_update=context.pending_upgrade,
prerelease=prerelease,
selected_tag=context.status.selected_tag,
version_available=context.releases.last_release,
version_installed=context.display_installed_version,
)
return render
except Exception as exception:
Logger("hacs.template").debug(exception)
return content
@@ -0,0 +1,27 @@
"""Helper functions: misc"""
import semantic_version
def get_repository_name(
hacs_manifest, repository_name: str, category: str = None, manifest: dict = None
) -> str:
"""Return the name of the repository for use in the frontend."""
if hacs_manifest.name is not None:
return hacs_manifest.name
if category == "integration":
if manifest:
if "name" in manifest:
return manifest["name"]
return repository_name.replace("-", " ").replace("_", " ").title()
def version_is_newer_than_version(new: str, old: str) -> bool:
"""Return a bool if source is newer than target, will also be true if identical."""
if not isinstance(new, str) or not isinstance(old, str):
return False
if new == old:
return True
return semantic_version.Version.coerce(new) > semantic_version.Version.coerce(old)
@@ -0,0 +1,262 @@
"""Blueprint for HacsWebResponse."""
import os
from time import time
from homeassistant.components.http import HomeAssistantView
from jinja2 import Environment, PackageLoader
from aiohttp import web
from integrationhelper import Logger
from .hacsbase import Hacs
WEBRESPONSE = {}
def webresponse(classname):
"""Decorator used to register Web Responses."""
WEBRESPONSE[classname.endpoint] = classname
return classname
class HacsExperimental(HomeAssistantView, Hacs):
"""Base View Class for HACS."""
requires_auth = False
name = "hacs"
url = r"/hacs_experimental/{requested_file:.+}"
def __init__(self):
"""Initialize."""
self.logger = Logger("hacs.http")
async def get(self, request, requested_file): # pylint: disable=unused-argument
"""Handle HACS Experimental Web requests."""
servefile = f"{self.system.config_path}/custom_components/hacs/frontend/experimental/{requested_file}"
# Serve .gz file if it exist
if os.path.exists(f"{servefile}.gz"):
servefile += ".gz"
if os.path.exists(servefile):
return web.FileResponse(servefile)
else:
return web.Response(status=404)
class HacsWebResponse(HomeAssistantView, Hacs):
"""Base View Class for HACS."""
requires_auth = False
name = "hacs"
def __init__(self):
"""Initialize."""
self.logger = Logger("hacs.http")
self.url = self.hacsweb + "/{path:.+}"
self.endpoint = None
self.postdata = None
self.raw_headers = None
self.repository_id = None
self.request = None
self.requested_file = None
async def get(self, request, path): # pylint: disable=unused-argument
"""Handle HACS Web requests."""
if self.system.disabled:
return web.Response(status=404)
self.endpoint = path.split("/")[0]
self.raw_headers = request.raw_headers
self.request = request
self.requested_file = path.replace(self.endpoint + "/", "")
self.repository_id = path.replace(self.endpoint + "/", "")
if self.endpoint != "static":
self.logger.debug(f"Endpoint ({self.endpoint}) called")
if self.endpoint in WEBRESPONSE:
try:
response = WEBRESPONSE[self.endpoint]
response = await response.response(self)
except Exception as exception:
render = self.render("error", message=exception)
return web.Response(
body=render, content_type="text/html", charset="utf-8"
)
else:
# Return default response.
response = await WEBRESPONSE["generic"].response(self)
# set headers
response.headers["Cache-Control"] = "no-cache, must-revalidate, s-max_age=0"
response.headers["Pragma"] = "no-cache"
# serve the response
return response
def render(self, templatefile, location=None, repository=None, message=None):
"""Render a template file."""
loader = Environment(loader=PackageLoader("custom_components.hacs.frontend"))
template = loader.get_template(templatefile + ".html")
return template.render(
{
"hacs": self,
"location": location,
"repository": repository,
"message": message,
"timestamp": time(),
}
)
class HacsPluginView(HacsWebResponse):
"""Serve plugins."""
name = "hacs:plugin"
def __init__(self):
"""Initialize."""
self.url = r"/community_plugin/{requested_file:.+}"
async def get(self, request, requested_file): # pylint: disable=unused-argument
"""Serve plugins for lovelace."""
try:
# Strip '?' from URL
if "?" in requested_file:
requested_file = requested_file.split("?")[0]
file = f"{self.system.config_path}/www/community/{requested_file}"
# Serve .gz if it exist
if os.path.exists(file + ".gz"):
file += ".gz"
response = None
if os.path.exists(file):
self.logger.debug("Serving {} from {}".format(requested_file, file))
response = web.FileResponse(file)
response.headers["Cache-Control"] = "max-age=0, must-revalidate"
else:
self.logger.error(f"Tried to serve up '{file}' but it does not exist")
response = web.Response(status=404)
except Exception as error: # pylint: disable=broad-except
self.logger.debug(
"there was an issue trying to serve {} - {}".format(
requested_file, error
)
)
response = web.Response(status=404)
return response
class HacsPlugin(HacsPluginView):
"""Alias for HacsPluginView."""
def __init__(self):
"""Initialize."""
self.url = r"/hacsplugin/{requested_file:.+}"
@webresponse
class Settings(HacsWebResponse):
"""Serve HacsSettingsView."""
endpoint = "settings"
async def response(self):
"""Serve HacsOverviewView."""
message = self.request.rel_url.query.get("message")
render = self.render("settings", "settings", message=message)
return web.Response(body=render, content_type="text/html", charset="utf-8")
@webresponse
class Static(HacsWebResponse):
"""Serve static files."""
endpoint = "static"
async def response(self):
"""Serve static files."""
servefile = f"{self.system.config_path}/custom_components/hacs/frontend/elements/{self.requested_file}"
if os.path.exists(servefile + ".gz"):
return web.FileResponse(servefile + ".gz")
else:
if os.path.exists(servefile):
return web.FileResponse(servefile)
else:
return web.Response(status=404)
@webresponse
class Store(HacsWebResponse):
"""Serve HacsOverviewView."""
endpoint = "store"
async def response(self):
"""Serve HacsStoreView."""
render = self.render("overviews", "store")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@webresponse
class Overview(HacsWebResponse):
"""Serve HacsOverviewView."""
endpoint = "overview"
async def response(self):
"""Serve HacsOverviewView."""
render = self.render("overviews", "overview")
return web.Response(body=render, content_type="text/html", charset="utf-8")
@webresponse
class Repository(HacsWebResponse):
"""Serve HacsRepositoryView."""
endpoint = "repository"
async def response(self):
"""Serve HacsRepositoryView."""
message = self.request.rel_url.query.get("message")
repository = self.get_by_id(str(self.repository_id))
if repository is None:
self.logger.error(f"No repository found with ID {str(self.repository_id)}")
return web.Response(status=404)
if not repository.status.updated_info:
await repository.update_repository()
repository.status.updated_info = True
await self.data.async_write()
if repository.status.new:
repository.status.new = False
await self.data.async_write()
render = self.render("repository", repository=repository, message=message)
return web.Response(body=render, content_type="text/html", charset="utf-8")
@webresponse
class Error(HacsWebResponse):
"""Serve error page."""
endpoint = "error"
async def response(self):
"""Serve error page."""
# Generate content
content = """
<div class='center-align' style='margin-top: 20px'>
<img rel="noreferrer" src='https://i.pinimg.com/originals/ec/85/67/ec856744fac64a5a9e407733f190da5a.png'>
</div>
"""
return web.Response(
body=self.render("error", message=content),
content_type="text/html",
charset="utf-8",
)
@@ -0,0 +1,23 @@
{
"domain": "hacs",
"name": "HACS (Home Assistant Community Store)",
"documentation": "https://hacs.xyz",
"dependencies": [
"websocket_api",
"frontend",
"persistent_notification"
],
"config_flow": true,
"codeowners": [
"@ludeeus"
],
"homeassistant": "0.98.0",
"requirements": [
"aiofiles==0.4.0",
"attrs==19.2.0",
"aiogithubapi==0.4.2",
"backoff==1.8.0",
"integrationhelper==0.2.2",
"semantic_version==2.8.2"
]
}
@@ -0,0 +1,6 @@
"""Initialize repositories."""
from .theme import HacsTheme
from .integration import HacsIntegration
from .python_script import HacsPythonScript
from .appdaemon import HacsAppdaemon
from .plugin import HacsPlugin
@@ -0,0 +1,87 @@
"""Class for appdaemon apps in HACS."""
from .repository import HacsRepository, register_repository_class
@register_repository_class
class HacsAppdaemon(HacsRepository):
"""Appdaemon apps in HACS."""
category = "appdaemon"
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.information.full_name = full_name
self.information.category = self.category
self.content.path.local = self.localpath
self.content.path.remote = "apps"
@property
def localpath(self):
"""Return localpath."""
return f"{self.system.config_path}/appdaemon/apps/{self.information.name}"
async def validate_repository(self):
"""Validate."""
await self.common_validate()
# Custom step 1: Validate content.
addir = await self.repository_object.get_contents("apps", self.ref)
if not isinstance(addir, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.path.remote = addir[0].path
self.information.name = addir[0].name
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.system.status.startup:
self.logger.error(error)
return self.validate.success
async def registration(self):
"""Registration."""
if not await self.validate_repository():
return False
# Run common registration steps.
await self.common_registration()
# Set local path
self.content.path.local = self.localpath
async def update_repository(self):
"""Update."""
if self.github.ratelimits.remaining == 0:
return
await self.common_update()
# Get appdaemon objects.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "apps":
addir = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.path.remote = addir[0].path
self.information.name = addir[0].name
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects:
self.content.files.append(filename.name)
# Set local path
self.content.path.local = self.localpath
@@ -0,0 +1,164 @@
"""Class for integrations in HACS."""
import json
from aiogithubapi import AIOGitHubException
from homeassistant.loader import async_get_custom_components
from .repository import HacsRepository, register_repository_class
@register_repository_class
class HacsIntegration(HacsRepository):
"""Integrations in HACS."""
category = "integration"
def __init__(self, full_name):
"""Initialize."""
super().__init__()
self.information.full_name = full_name
self.information.category = self.category
self.domain = None
self.content.path.remote = "custom_components"
self.content.path.local = self.localpath
@property
def localpath(self):
"""Return localpath."""
return f"{self.system.config_path}/custom_components/{self.domain}"
@property
def config_flow(self):
"""Return bool if integration has config_flow."""
if self.manifest:
if self.information.full_name == "hacs/integration":
return False
return self.manifest.get("config_flow", False)
return False
async def validate_repository(self):
"""Validate."""
await self.common_validate()
# Attach repository
if self.repository_object is None:
self.repository_object = await self.github.get_repo(
self.information.full_name
)
# Custom step 1: Validate content.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "custom_components":
ccdir = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
if not isinstance(ccdir, list):
self.validate.errors.append("Repostitory structure not compliant")
for item in ccdir or []:
if item.type == "dir":
self.content.path.remote = item.path
break
if self.repository_manifest.zip_release:
self.content.objects = self.releases.last_release_object.assets
else:
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
self.content.files = []
for filename in self.content.objects or []:
self.content.files.append(filename.name)
if not await self.get_manifest():
self.validate.errors.append("Missing manifest file.")
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.system.status.startup:
self.logger.error(error)
return self.validate.success
async def registration(self):
"""Registration."""
if not await self.validate_repository():
return False
# Run common registration steps.
await self.common_registration()
# Get the content of the manifest file.
await self.get_manifest()
# Set local path
self.content.path.local = self.localpath
async def update_repository(self):
"""Update."""
if self.github.ratelimits.remaining == 0:
return
await self.common_update()
# Get integration objects.
if self.repository_manifest:
if self.repository_manifest.content_in_root:
self.content.path.remote = ""
if self.content.path.remote == "custom_components":
ccdir = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
if not isinstance(ccdir, list):
self.validate.errors.append("Repostitory structure not compliant")
self.content.path.remote = ccdir[0].path
try:
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
except AIOGitHubException:
return
self.content.files = []
for filename in self.content.objects or []:
self.content.files.append(filename.name)
await self.get_manifest()
# Set local path
self.content.path.local = self.localpath
async def reload_custom_components(self):
"""Reload custom_components (and config flows)in HA."""
self.logger.info("Reloading custom_component cache")
del self.hass.data["custom_components"]
await async_get_custom_components(self.hass)
async def get_manifest(self):
"""Get info from the manifest file."""
manifest_path = f"{self.content.path.remote}/manifest.json"
try:
manifest = await self.repository_object.get_contents(
manifest_path, self.ref
)
manifest = json.loads(manifest.content)
except Exception: # pylint: disable=broad-except
return False
if manifest:
self.manifest = manifest
self.information.authors = manifest["codeowners"]
self.domain = manifest["domain"]
self.information.name = manifest["name"]
self.information.homeassistant_version = manifest.get("homeassistant")
# Set local path
self.content.path.local = self.localpath
return True
return False
@@ -0,0 +1,41 @@
"""
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
@@ -0,0 +1,169 @@
"""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
@@ -0,0 +1,79 @@
"""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)
@@ -0,0 +1,697 @@
"""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("&lt;", "<")
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", "<&#8205;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
@@ -0,0 +1,78 @@
"""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)
@@ -0,0 +1,80 @@
"""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}
@@ -0,0 +1,15 @@
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'
@@ -0,0 +1,95 @@
"""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,
)
@@ -0,0 +1,19 @@
"""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)
@@ -0,0 +1,112 @@
{
"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"
}
}
@@ -0,0 +1,316 @@
"""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))
@@ -0,0 +1,9 @@
{
"domain": "yandex_maps",
"name": "Yandex Maps",
"documentation": "https://github.com/custom-components/sensor.yandex_maps",
"dependencies": [],
"codeowners": [],
"requirements": []
}
@@ -0,0 +1,122 @@
"""
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
+2
View File
@@ -85,9 +85,11 @@ tag_nut_guest: xxxxxxxxx
###################################
home_latitude: 1.0
home_longitude: 1.0
home_coords: xxxxxxxxx
work_latitude: 1.0
work_longitude: 1.0
work_coords: xxxxxxxxx
###################################
# SYNOLOGY SETTINGS #
+13 -3
View File
@@ -55,7 +55,6 @@ views:
- light.bath
- light.philips_led
- sensor.airfresher_remain
- sensor.toilet_presence
- type: entities
title: "Прихожая"
show_header_toggle: false
@@ -145,6 +144,7 @@ views:
- sensor.presence_hall
- sensor.presence_kitchen
- sensor.presence_balcony
- sensor.toilet_presence
- type: entities
title: "Электропитание"
show_header_toggle: false
@@ -339,8 +339,7 @@ views:
- input_boolean.mainroom_climate_control
- switch.domofon_mode
- type: entities
title: "Настройки от\
четов"
title: "Настройки отчетов"
show_header_toggle: true
entities:
- input_boolean.security_sensors_report
@@ -357,6 +356,17 @@ views:
- input_boolean.tag_nut_blue
- input_boolean.tag_nut_white
- input_boolean.tag_nut_guest
- type: entities
title: "Аптайм"
show_header_toggle: false
entities:
- sensor.uptime_airfresher
- sensor.uptime_bath
- sensor.uptime_domofon
- sensor.uptime_kitchen_led
- sensor.uptime_nest
- sensor.uptime_bathlight
- sensor.uptime_kitchen
- title: "Информация"
icon: mdi:monitor-multiple
cards: