This commit is contained in:
Simon 2025-04-14 21:09:34 +02:00
parent d411978b67
commit ff284125db
513 changed files with 4589 additions and 30513 deletions

8
.env
View File

@ -1,6 +1,7 @@
UID=1000
GID=1004
DIR=/srv/docker
DIR_LOCAL=/opt/docker_data
DOMAIN=milvert.com
DOMAIN_PEEK=peekskog.se
@ -51,3 +52,10 @@ VW_DB_NAME='vwfriend'
VW_DB_USER='vwfriend'
VW_DB_PASSWORD='icCJ8iwKJBeRBg'
ADDITIONAL_PARAMETERS=-vv --mqttbroker mqtt --mqttport 1883 -mu simon -mp bajsa123
DEBUG_ABOVE=--mqttbroker mqtt --mqttport 1883 -mu simon -mp bajsa123
UPTIME_KUMA_PASSWORD=bajsa123
UPTIME_KUMA_USER=simon@milvert.com

View File

@ -18,19 +18,23 @@ dns:
port: 53
anonymize_client_ip: false
ratelimit: 20
ratelimit_subnet_len_ipv4: 24
ratelimit_subnet_len_ipv6: 56
ratelimit_whitelist: []
refuse_any: true
upstream_dns:
- https://dns10.quad9.net/dns-query
- https://dns.cloudflare.com/dns-query
- https://dns.google/dns-query
- https://security.cloudflare-dns.com/dns-query
upstream_dns_file: ""
bootstrap_dns:
- 9.9.9.10
- 149.112.112.10
- 2620:fe::10
- 2620:fe::fe:10
fallback_dns: []
all_servers: false
fastest_addr: false
fallback_dns:
- 1.1.1.2
upstream_mode: load_balance
fastest_timeout: 1s
allowed_clients: []
disallowed_clients: []
@ -59,12 +63,14 @@ dns:
bootstrap_prefer_ipv6: false
upstream_timeout: 10s
private_networks: []
use_private_ptr_resolvers: true
use_private_ptr_resolvers: false
local_ptr_upstreams: []
use_dns64: false
dns64_prefixes: []
serve_http3: false
use_http3_upstreams: false
serve_plain_dns: true
hostsfile_enabled: true
tls:
enabled: false
server_name: ""
@ -81,12 +87,14 @@ tls:
private_key_path: ""
strict_sni_check: false
querylog:
dir_path: ""
ignored: []
interval: 2160h
size_memory: 1000
enabled: true
file_enabled: true
statistics:
dir_path: ""
ignored: []
interval: 24h
enabled: true
@ -161,6 +169,7 @@ clients:
hosts: true
persistent: []
log:
enabled: true
file: ""
max_backups: 0
max_size: 100
@ -172,4 +181,4 @@ os:
group: ""
user: ""
rlimit_nofile: 0
schema_version: 27
schema_version: 28

View File

@ -1,9 +1,9 @@
###############################################################
# Authelia configuration #
###############################################################
server:
address: 'tcp://:9091'
server.host: 0.0.0.0
server.port: 9091
log:
level: info
@ -14,6 +14,13 @@ log:
# https://docs.authelia.com/configuration/miscellaneous.html#default-redirection-url
default_redirection_url: https://authelia.milvert.com
webauthn: #FIDO2 Authentication
disable: false
display_name: Authelia
attestation_conveyance_preference: direct
user_verification: required
timeout: 60s
totp:
issuer: authelia.com
period: 30
@ -56,6 +63,10 @@ access_control:
policy: two_factor
- domain: "milvert.com"
policy: two_factor
- domain:
- 'uptime.example.com'
subject: 'oauth2:client:uptime-kuma'
policy: 'one_factor'
session:
name: authelia_session
@ -85,10 +96,11 @@ storage:
#path: /config/db.sqlite3
postgres:
# MySQL allows running multiple authelia instances. Create database and enter details below.
host: postgres
port: 5432
database: authelia
username: authelia
address: 'tcp://postgres:5432'
#host: postgres
#port: 5432
database: 'authelia'
username: 'authelia'
# Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
# password: use docker secret file instead AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE
@ -104,3 +116,118 @@ notifier:
# For testing purpose, notifications can be sent in a file. Be sure map the volume in docker-compose.
filesystem:
filename: /tmp/authelia/notification.txt
identity_providers:
oidc:
hmac_secret: 'akVs2Tr510MpfECDciJhpSI6SiHKhdiGefG2wMzPSuUhRlWNB0VNwDTxsFNZrRrw'
jwks:
- key_id: 'milvert_authelia_oidc_key'
algorithm: 'RS256'
use: 'sig'
key: |
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEA1Yxr3WL300u245uZrBOgZtX63IwtFT9NDighnIz/PcFiYbUw
lsrXi5HBJXuIEbxJDcdSdvPhusx08wizPuEnTLVphOgwQ8Xhab3qKLfmwW8yHGsX
9+osJNhAzmXJXAMbjgz2Rqd7tuOT2PkyYF707FQBRlYNhcMfi06WVhlo4WFPab95
seKmj3bSIHlmbPnrL9GqOhAtV018COCbMXM2pu6yQOTkdSltZyg5L1+QkSf2MAUN
VjjTzWbjI8en9vQfZZjA1h7O0bpR/WOmPv9S+SdmnHE9FewOXux3aljC0qrTHeAh
GIjq+8fzREN0Xvvqox5ZWwBKmPax4ed448Vm/8U3rZQ02Ftpr97w2inL/MT0UTmS
wKudIlzmkuejqy0jiZ/aAX6JpE5OLsm1zhJSLJFx/vNxByh1kd2EFon22bUa5ZLF
buVU1WMkhr1Nc8vTCgnr0Y3XKbB1koGJFwK6lg9L0Tstrt+SY34K2iRtFce/Lt2k
KFkO+hfx3J8hRg2DcazR9bZTsjsK+OHw9sNaFMrkAf4Rd8Z27yRtSRdZXefgz88G
1xjqgdZmjupKgRPJzCro4hbvmH6x1L8Q3ZzR5fstP8rui8m9UIsSCwLdzGlc7x7L
toQckn+EFlZ0kLl1e3nlMDUpOaezx7TXt1OxlJtiX7MmGfhUcY+8k3+JS+0CAwEA
AQKCAgALpHU5A61JOsaE0DXnmMZU9PORVJ8cmm/kBrIWa+V6g3GOrCczAWoQ9OhX
181KUj6VXrmTPKag3Ya+vUmwcuwGlZlkIpiHCWHJtw3uHGCbSSmtPPV4ka7Ib3XR
CuGYf57f9rryjS0EgpHL8YIamPK3c7kCEaz5DvNIUAeIOChsqTaAKG1FEntMNQkt
thCsfk+hMsgaFEm0icfqX/x2DLb9EORs/02pSZHqXtoHSCmEkG4ungflHIIHn8Vg
bQEuSI7xpgtVYSabbpILw4QLyTXH2asRemb/K/h4mmHETYSJocCSz2ZehRBym6sa
nKbaitd2/y+V84Udo8186HbBzEBaNekr8IVDfY1NDi99ZgSUJBS0jDCUb84A6Ucf
CRDZofvjNKV90x7wlZPz5T8N+lpDSH+ThwU0T1k8aydRA6DL/otFNfOS6vaqBdg3
Gvpd7SQUT88u1l7rVZwsJ+uGBq9Sx+ScCnjl04jc62hDUy51hR+mOfrWNCJGqfJ9
YJlaH2bZJuzKAyXAEYjJJuYfYPpTDVZ2glzSM72ytmPZseB6KwDJ8gZFtbdHUi0V
eol49mOCKwBsaLjUh7rqix2WkO6yjcch17HrvpBEUZw+B0FIOPUdC2iH26lpOk0e
QiuAPXZXcch00ta9UMBUfr8O8LVznm6L751UdaYnpNbw+2VHrQKCAQEA9soiuZYt
jJmteohGKe2YOCOfzxirE2XqW8g5RWtWb84pEt+oZLEzrKFL4fvKYCEumfnLIrTe
E6XrwVNbmRLxhRzPJi2ZgAqImy138BqeY9ygorUDKJP343JMBOKQvxCXF/ZvYYqn
AXN6xt+1X2nlgBxWUJr7oqp2DJ/X7rBH2xRB8UITPInZCv9gxgRTWe5j8GToZJ2b
S8VxgETl2IyBRE9H6knRZibD8uZKksLCPFIQdf/dkneiPTVW/PhvLzbASY3jOLJT
O62xTkeykGEsdgAVYtBYuBrP86ujHHaqO1nGVMAYXVINNukrqXuF3n8XXCCmgFue
Ibdus2UDct/7qwKCAQEA3YSyEVg8aSiF+cGqv1CpJFzdM169zDNxFooD7b7oj/FX
oMZMKQ1EGcqWJ8ivseM1OYoc2FlVZF+bem1VNEDcswTEuqI7YnRJcua5vbBMBQP+
FO0a8rbI3poctY/1hPt/oZ0twX7mBMLzljJ4kQLaC4LLQDlhhP4SriqWoXx6BfFV
AZEbcNlzyOHGIUdA9ahyVB5isYC3ru6lZltAg+2+zHucLvNZ0H/jVAgjH8qOxoZh
m6XILdQVdFMhZqmdLWPfFgZGqL/zc3qHrIWWvunawcIEnUZvVkTnCTSfIFvfsErJ
jlT7hVUxNLQqed/DIsX9bz/Vj0Uj/7IOCcwBFiv+xwKCAQEAgUo4kv3IcyZ3qgAG
siSVYKPZL0f/KDR2aQsmZeXr7LsW7ZpawBXNupzuAkBFL8NyhJxG/bG/vo9oSWoA
TNuNyGzlYVz2VAwwsQtLEHEBldOaHGO0eee7MF9K0CxaJJ7uaVFj3Lgk8i+rnNDK
VmhGIa220pe/SOMA4XBEUfnsSyv7uAcjyM129boA2vydJjosBV74GO4w06tm4Qo3
WBGUD1Nxm558o4WflntriiOaWrurgAZB8F/YkTSGlBUbOqL2bhJ1fdh+nn9KqnYJ
aHZgMpmsmo4ITLtPQpsi4uCQInPP4cqZeRppbeEOTMY3xe7TMCKy2AAnggZ1amp7
Og157QKCAQBvfoyJwlmVrDnSvbGNXbHkhYEoi/UHxJSU/Z6AmWhAmv66A1TuDKlM
OfVdzNrxtCRj86oVEtGyFZUSB9ms1UDAMhQ6w9ET+ufFF3BBk2yP0bSfH8BCjdGI
iRUOJYk0B8nztEMFczOfDejAnmKkykSpKonWp4r3/1Gzq+fpG9fnCdL5WOnw4OIw
J8MrmMuPWdtBj5GpOdo6CA/j9uYAATfZgBXaY82+7b+j2fyj0bYPIjAawVSCDI9H
31eebpyX7f6o/TuvT/3fD7seEJcRPG9IurjL2FnNmByZO40kIlnyR5IvO4LlVz3P
Ayel9AQpinHG/uAknm5CEoKSV8XsPPSdAoIBAQDweDT7RGHYHQp0INcQ3JxjrfN3
PcaeVths+7KA+pAq+id1grv48Yg+bo/vfoA25SSV6UrtWBW8JUUtFcRIH+UFnQ7c
rZkmI/l6lzdyJ3akzIJRAKvo7JGmT4NqTCjmug0Oo6feTjwuBisGRA7UFB/7gjJa
v9IhIt51N7Dl+SHK+drYGoErbzurxCOmuE0+GCnZ2qvdafVbk6zh4U2pZ2feOfqu
mPM3FMJdnSrXYtVWAY7hfSIsF/Ndh+kdkQi/s6TsZHqZ3PLTKWUk5ETwFpTqEXaM
DsGaWut89Ik9YrcAQVKXez5jVJRsYXeMCznEXed6fLssXgmJT2OlrEgSQhEj
-----END RSA PRIVATE KEY-----
enable_client_debug_messages: true
minimum_parameter_entropy: 8
enforce_pkce: 'public_clients_only'
enable_pkce_plain_challenge: false
enable_jwt_access_token_stateless_introspection: false
discovery_signed_response_alg: 'none'
discovery_signed_response_key_id: ''
require_pushed_authorization_requests: false
authorization_policies:
policy_name:
default_policy: 'one_factor'
rules:
- policy: 'deny'
subject: 'group:services'
lifespans:
access_token: '1h'
authorize_code: '1m'
id_token: '1h'
refresh_token: '90m'
cors:
endpoints:
- 'authorization'
- 'token'
- 'revocation'
- 'introspection'
allowed_origins:
- 'https://authelia.milvert.com'
allowed_origins_from_client_redirect_uris: false
clients:
- client_id: 'aN0VgMKamGZvleUplkT3W7kvXJmvsWmy4C9Obd6u5XGqL7A9B7CP2xxdSIv4ljIA'
client_name: 'Grafana'
client_secret: '$pbkdf2-sha512$310000$X53J.7eRdnYPuVSG6Uc0vw$y/PP9Wt5sHUrovp5hnXcJe6gias2t9h.PYj6iP0cMS1F2pDd98tzamSuoaU2b89vGONWdX0MaLKVs.6MFzCLEg'
public: false
authorization_policy: 'one_factor'
redirect_uris:
- 'https://data.milvert.com/login/generic_oauth'
scopes:
- 'openid'
- 'profile'
- 'groups'
- 'email'
userinfo_signed_response_alg: 'none'
- client_id: 'MlMNM1K1vGR3wHBPNsZZ7J66u1cGkMGlzBoZoYJwuc80quRsjrlV9jEZlMLTTGmT'
client_name: 'Gitea'
client_secret: '$pbkdf2-sha512$310000$E2hUgSHeRFIhAr5bQsDAFg$1qPDiXvtmvwVhwWb./gie6F2CCI80oQQkXln.xd.q.HNVI00kn1D5esj0faJrJhHgNjV0udqrBD5SdIVD8vXow'
public: false
authorization_policy: 'one_factor'
redirect_uris:
- 'https://gitea.milvert.com/user/oauth2/Authelia/callback'
scopes:
- 'openid'
- 'email'
- 'profile'
userinfo_signed_response_alg: 'none'

View File

@ -42,7 +42,7 @@ services:
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.middlewares.webdb-mid.ipwhitelist.sourcerange=127.0.0.1/32, 10.0.0.1/24"
- "traefik.http.middlewares.webdb-mid.ipallowlist.sourcerange=127.0.0.1/32, 10.0.0.1/24"
- "traefik.http.routers.webdb-secure.middlewares=webdb-mid"
- "traefik.http.routers.webdb-secure.entrypoints=web-secure"
- "traefik.http.routers.webdb-secure.rule=Host(`webdb.${DOMAIN}`)"

View File

@ -3,7 +3,7 @@ version: '3'
services:
node-red:
image: nodered/node-red:3.0.2
image: nodered/node-red:3.1.6
# image: nodered/node-red-dev:3.0.0-beta.4-14
container_name: "node-red"
@ -57,7 +57,7 @@ services:
- "traefik.enable=false"
- "traefik.http.services.landet_domo-service.loadbalancer.server.port=8080"
- "traefik.http.routers.landet_domo-secure.entrypoints=web-secure"
- "traefik.http.routers.landet_domo-secure.rule=Host(`landet.${DOMAIN}`)"
- "traefik.http.routers.landet_domo-secure.rule=Host(`landet_old.${DOMAIN}`)"
- "traefik.http.routers.landet_domo-secure.tls.certresolver=milvert_dns"
- "traefik.http.routers.landet_domo-secure.tls=true"

View File

@ -3,7 +3,7 @@ version: '3'
services:
nextcloud:
container_name: nextcloud
image: nextcloud:25
image: nextcloud:27
logging:
driver: "json-file"
options:
@ -49,9 +49,9 @@ services:
- NEXTCLOUD_TRUSTED_DOMAINS=moln.milvert.com
- TRUSTED_PROXIES=172.19.0.0/16
volumes:
- ${DIR}/nextcloud-www:/var/www/html
# - ${DIR}/nextcloud-www:/var/www/html
- ./nextcloud/cronjob:/var/spool/cron/crontabs/www-data
- /srv/owncloud:/var/www/html/data
#- /srv/owncloud:/var/www/html/data
- /media/NAS:/media/NAS
networks:

View File

@ -11,7 +11,9 @@ services:
max-file: "3"
networks:
- backend
image: koenkk/zigbee2mqtt:1.33.2
ports:
- "8088:8080"
image: koenkk/zigbee2mqtt:2.1
restart: always
volumes:
- ./zigbee_home_2:/app/data
@ -41,7 +43,7 @@ services:
max-file: "5"
networks:
- backend
image: koenkk/zigbee2mqtt:1.33.2
image: koenkk/zigbee2mqtt:2.1
restart: always
volumes:
- ${DIR}/zigbee2matt:/app/data
@ -65,7 +67,7 @@ services:
influx:
image: influxdb:2.6
image: influxdb:2.7
container_name: influxdb
logging:
driver: "json-file"
@ -136,7 +138,7 @@ services:
#- "traefik.http.routers.influx-secure.middlewares=localNetwork@file"
gitea:
image: gitea/gitea:1.17
image: gitea/gitea:1.21
container_name: gitea
logging:
driver: "json-file"
@ -171,7 +173,7 @@ services:
mqtt:
# image: eclipse-mosquitto:1.6.13
image: eclipse-mosquitto:2.0.17
image: eclipse-mosquitto:2.0.18
container_name: mqtt
logging:
driver: "json-file"
@ -248,71 +250,43 @@ services:
labels:
- diun.enable=true
ddns-updater:
image: qmcgaw/ddns-updater:256
container_name: ddns-updater
restart: always
networks:
- backend
ports:
- 8000:8000/tcp
volumes:
- ${DIR}/ddns-updater:/updater/data
environment:
PUID: 1000
PGID: 1004
TZ: ${TZ}
PERIOD: 1h
UPDATE_COOLDOWN_PERIOD: 5m
PUBLICIP_DNS_TIMEOUT: 3s
HTTP_TIMEOUT: 10s
# Web UI
LISTENING_PORT: 8000
# Backup
BACKUP_PERIOD: 96h # 0 to disable
BACKUP_DIRECTORY: /updater/data/backups
# Other
LOG_LEVEL: info
SHOUTRRR_ADDRESSES: $TGRAM_SHOUTRRR_ADDRESS
labels:
- diun.enable=true
pihole:
image: pihole/pihole:2023.03.1
container_name: pihole
ports:
- "53:53/tcp"
- "53:53/udp"
- "8001:80"
dns:
- 127.0.0.1
- 9.9.9.9
environment:
- TZ=${TZ}
- WEBPASSWORD=${PIHOLE_PW}
- ServerIP=10.0.201
- PIHOLE_DNS_=127.0.0.1;9.9.9.9;149.112.112.112;1.1.1.1
- DNSSEC='true'
- VIRTUAL_HOST=pihole.milvert.com # Same as port traefik config
- WEBTHEME=default-dark
- PIHOLE_DOMAIN=milvert.com
volumes:
- ${DIR}/pihole/etc:/etc/pihole
- ${DIR}/pihole/dns:/etc/dnsmasq.d
restart: unless-stopped
networks:
backend:
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.middlewares.pihole-admin.addprefix.prefix=/admin"
- "traefik.http.services.pihole.loadbalancer.server.port=80"
- "traefik.http.routers.pihole-secure.middlewares=localNetwork@file"
- "traefik.http.routers.pihole-secure.entrypoints=web-secure"
- "traefik.http.routers.pihole-secure.rule=Host(`pihole.${DOMAIN}`)"
- "traefik.http.routers.pihole-secure.tls.certresolver=milvert_dns"
- "traefik.http.routers.pihole-secure.tls=true"
- "traefik.http.routers.pihole-secure.middlewares=chain-no-auth@file"
#pihole:
#image: pihole/pihole:2023.03.1
#container_name: pihole
#ports:
#- "53:53/tcp"
#- "53:53/udp"
#- "8001:80"
#dns:
#- 127.0.0.1
#- 9.9.9.9
#environment:
#- TZ=${TZ}
#- WEBPASSWORD=${PIHOLE_PW}
#- ServerIP=10.0.201
#- PIHOLE_DNS_=127.0.0.1;9.9.9.9;149.112.112.112;1.1.1.1
#- DNSSEC='true'
##- VIRTUAL_HOST=pihole.milvert.com # Same as port traefik config
#- WEBTHEME=default-dark
#- PIHOLE_DOMAIN=milvert.com
#volumes:
#- ${DIR}/pihole/etc:/etc/pihole
#- ${DIR}/pihole/dns:/etc/dnsmasq.d
#restart: unless-stopped
#networks:
#backend:
#labels:
#- diun.enable=true
#- "traefik.enable=true"
#- "traefik.http.middlewares.pihole-admin.addprefix.prefix=/admin"
#- "traefik.http.services.pihole.loadbalancer.server.port=80"
#- "traefik.http.routers.pihole-secure.middlewares=localNetwork@file"
#- "traefik.http.routers.pihole-secure.entrypoints=web-secure"
#- "traefik.http.routers.pihole-secure.rule=Host(`pihole.${DOMAIN}`)"
#- "traefik.http.routers.pihole-secure.tls.certresolver=milvert_dns"
#- "traefik.http.routers.pihole-secure.tls=true"
#- "traefik.http.routers.pihole-secure.middlewares=chain-no-auth@file"
networks:
frontend:

View File

@ -33,6 +33,8 @@ services:
image: containous/whoami
networks:
- backend
ports:
- 8005:80
labels:
- diun.enable=true
- "traefik.enable=true"

View File

@ -1,8 +1,13 @@
version: "3.9"
secrets:
authelia_jwt_secret:
file: $SECRETSDIR/authelia_jwt_secret
authelia_oidc_key_secret:
file: $SECRETSDIR/authelia_oidc_key
authelia_oidc_hamc_secret:
file: $SECRETSDIR/authelia_oidc_hamc
authelia_oidc_pem_secret:
file: $SECRETSDIR/authelia_oidc_pem
authelia_session_secret:
file: $SECRETSDIR/authelia_session_secret
authelia_storage_postgres_password:
@ -32,15 +37,13 @@ x-common-keys-monitoring: &common-keys-monitoring
networks:
- backend
security_opt:
- no-new-privileges:true
restart: always
- no-new-privileges:true restart: always
services:
reverse-proxy:
# The official v2.0 Traefik docker image
image: traefik:v2.10
#image: traefik:v2.11
image: traefik:v3.1
container_name: "traefik"
logging:
driver: "json-file"
@ -55,6 +58,8 @@ services:
# The HTTP port
- "80:80"
- "443:443"
# Insecure port
- "8080:8080"
# Influx
- "8086:8086"
# Mqtt
@ -63,23 +68,23 @@ services:
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
- ${DIR_LOCAL}/traefik/log:/log:rw
- ./traefik.yml:/etc/traefik/traefik.yml
- ./traefik:/rules
- ./static_config.yml:/etc/traefik/static_config.yml
# - "./log.json:/etc/traefik/log.json"
#- ./acme.json:/acme.json
- ./letsencrypt/acme.json:/letsencrypt/acme.json
- ${DIR}/traefik/log:/log
# - "./log.json:/etc/traefik/log.json"
# - ./acme.json:/acme.json
- ./letsencrypt/:/letsencrypt:rw
# - ./letsencrypt/acme_peek_staged.json:/letsencrypt/acme_peek_staged.json
environment:
- CF_API_EMAIL=simon@milvert.com
- CF_API_KEY=48d9ae3752afb6e73d99d23c432ba8e38b24c
#- CF_DNS_API_TOKEN=48d9ae3752afb6e73d99d23c432ba8e38b24c
- CF_DNS_API_TOKEN=m-X93yWXyvQ2vDhfNLURcQTWOqle13aBbw7g2Zxg
- CLOUDFLARE_IPS
- LOCAL_IPS
labels:
- diun.enable=true
- "traefik.http.routers.zighome-secure.middlewares=chain-authelia@file"
dns:
- 8.8.8.8
authelia:
image: authelia/authelia:4
@ -96,12 +101,16 @@ services:
- TZ=$TZ
- PUID=$PUID
- PGID=$PGID
- AUTHELIA_JWT_SECRET_FILE=/run/secrets/authelia_jwt_secret
- AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/run/secrets/authelia_jwt_secret
- AUTHELIA_SESSION_SECRET_FILE=/run/secrets/authelia_session_secret
- AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE=/run/secrets/authelia_storage_postgres_password
#- AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/run/secrets/authelia_notifier_smtp_password
- AUTHELIA_DUO_API_SECRET_KEY_FILE=/run/secrets/authelia_duo_api_secret_key
- AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/run/secrets/authelia_storage_encryption_key_file
# - AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE=/run/secrets/authelia_oidc_hamc_secret
#- AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER=/run/secrets/authelia_oidc_pem_secret
- AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_KEY=/run/secrets/authelia_oidc_key_secret
labels:
- diun.enable=true
- "traefik.enable=true"
@ -115,17 +124,55 @@ services:
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email" # yamllint disable-line rule:line-length
secrets:
- authelia_jwt_secret
- authelia_oidc_pem_secret
- authelia_oidc_hamc_secret
- authelia_oidc_key_secret
- authelia_session_secret
- authelia_storage_postgres_password
- authelia_notifier_smtp_password
- authelia_duo_api_secret_key
- authelia_storage_encryption_key_file
ddns-updater:
image: qmcgaw/ddns-updater:2.7
container_name: ddns-updater
restart: always
networks:
- backend
ports:
- 8000:8000/tcp
volumes:
- ${DIR_LOCAL}/ddns-updater:/updater/data
environment:
PUID: 1000
PGID: 1004
TZ: ${TZ}
PERIOD: 1h
UPDATE_COOLDOWN_PERIOD: 5m
PUBLICIP_DNS_TIMEOUT: 3s
HTTP_TIMEOUT: 10s
# Web UI
LISTENING_PORT: 8000
# Backup
BACKUP_PERIOD: 96h # 0 to disable
BACKUP_DIRECTORY: /updater/data/backups
# Other
LOG_LEVEL: info
SHOUTRRR_ADDRESSES: $TGRAM_SHOUTRRR_ADDRESS
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.routers.ddnsupdater.rule=Host(`ddns.${DOMAIN}`)"
- "traefik.http.routers.ddnsupdater.entrypoints=web-secure"
- "traefik.http.services.ddnsupdater.loadbalancer.server.port=8000"
- "traefik.http.routers.ddnsupdater.middlewares=chain-authelia@file"
- "traefik.http.routers.ddnsupdater.tls.certresolver=milvert_dns"
- "traefik.http.routers.ddnsupdater.tls=true"
adguard:
container_name: adguard
image: adguard/adguardhome:v0.107.40
image: adguard/adguardhome:v0.107.52
restart: unless-stopped
networks:
docker_vlan:
@ -145,9 +192,9 @@ services:
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.services.adguard.loadbalancer.server.port=3000"
- "traefik.http.services.adguard.loadbalancer.server.port=80"
- "traefik.http.routers.adguard.entrypoints=web-secure"
- "traefik.http.routers.adguard.rule=Host(`vwgrafana.${DOMAIN}`)"
- "traefik.http.routers.adguard.rule=Host(`adguard.${DOMAIN}`)"
- "traefik.http.routers.adguard.middlewares=chain-authelia@file"
- "traefik.http.routers.adguard.tls.certresolver=milvert_dns"
- "traefik.http.routers.adguard.tls=true"
@ -171,7 +218,7 @@ services:
networks:
- backend
volumes:
- ${DIR}/database:/var/lib/mysql:rw
- ${DIR_LOCAL}/database:/var/lib/mysql:rw
ports:
- "3307:3306"
labels:
@ -199,7 +246,7 @@ services:
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.middlewares.webdb-mid.ipwhitelist.sourcerange=127.0.0.1/32, 10.0.0.1/24"
- "traefik.http.middlewares.webdb-mid.ipallowlist.sourcerange=127.0.0.1/32, 10.0.0.1/24"
- "traefik.http.routers.webdb-secure.middlewares=webdb-mid"
- "traefik.http.routers.webdb-secure.entrypoints=web-secure"
- "traefik.http.routers.webdb-secure.rule=Host(`webdb.${DOMAIN}`)"
@ -226,7 +273,7 @@ services:
networks:
- backend
volumes:
- ${DIR}/database_pg/data:/var/lib/postgresql/data
- ${DIR_LOCAL}/database_pg/data:/var/lib/postgresql/data
labels:
- diun.enable=true
- "traefik.enable=false"
@ -282,7 +329,7 @@ services:
networks:
- backend
volumes:
- ${DIR}/redis:/var/lib/redis
- ${DIR_LOCAL}/redis:/var/lib/redis
#entrypoint: redis-server --appendonly yes --requirepass $REDIS_PASSWORD --maxmemory 512mb --maxmemory-policy allkeys-lru
labels:
- diun.enable=true
@ -295,7 +342,7 @@ services:
ha:
container_name: ha
image: homeassistant/home-assistant:2023.11
image: homeassistant/home-assistant:2025.2
restart: always
privileged: true
networks:
@ -395,11 +442,9 @@ services:
command:
- evcc
container_name: evcc
image: evcc/evcc:0.123.7
image: evcc/evcc:0.200.5
ports:
- 7070:7070/tcp
dns:
- 8.8.8.8
volumes:
- "./evcc/evcc.yaml:/etc/evcc.yaml:ro"
- ./evcc/evcc:/root/.evcc
@ -417,7 +462,7 @@ services:
- "traefik.http.routers.evcc.tls=true"
grafana:
image: grafana/grafana:10.0.0
image: grafana/grafana:10.3.1
container_name: grafana
logging:
driver: "json-file"
@ -431,7 +476,7 @@ services:
- ./grafana/grafana.ini:/etc/grafana/grafana.ini
# Data persistency
# sudo mkdir -p /srv/docker/grafana/data; chown 472:472 /srv/docker/grafana/data
- "${DIR}/grafana:/var/lib/grafana"
- "${DIR_LOCAL}/grafana:/var/lib/grafana"
labels:
- diun.enable=true
- "traefik.enable=true"
@ -441,6 +486,149 @@ services:
- "traefik.http.routers.grafana-secure.tls.certresolver=milvert_dns"
- "traefik.http.routers.grafana-secure.tls=true"
node-red:
image: nodered/node-red:3.1.6
# image: nodered/node-red-dev:3.0.0-beta.4-14
container_name: "node-red"
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "3"
networks:
- backend
environment:
- TZ=${TZ}
ports:
- "1881:1880"
#devices:
#- /dev/ttyAMA0
restart: unless-stopped
user: ${UID}
volumes:
- ${DIR_LOCAL}/nodered:/data
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.services.node-red-service.loadbalancer.server.port=1880"
- "traefik.http.routers.node-red-secure.entrypoints=web-secure"
- "traefik.http.routers.node-red-secure.rule=Host(`nodered.${DOMAIN}`)"
- "traefik.http.routers.node-red-secure.tls.certresolver=milvert_dns"
- "traefik.http.routers.node-red-secure.tls=true"
nextcloud:
container_name: nextcloud
image: nextcloud:28
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "3"
restart: always
ports:
- "8009:80"
networks:
- backend
depends_on:
- redis
- postgres
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.entrypoints=web-secure"
- "traefik.http.routers.nextcloud.middlewares=nextcloud,nextcloud_redirect"
- "traefik.http.routers.nextcloud.tls.certresolver=milvert_dns"
- "traefik.http.routers.nextcloud.rule=Host(`moln.${DOMAIN}`)"
- "traefik.http.middlewares.nextcloud.headers.customFrameOptionsValue=ALLOW-FROM https://milvert.com"
- "traefik.http.middlewares.nextcloud.headers.contentSecurityPolicy=frame-ancestors 'self' milvert.com"
- "traefik.http.middlewares.nextcloud.headers.stsSeconds=155520011"
- "traefik.http.middlewares.nextcloud.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.nextcloud.headers.stsPreload=true"
- "traefik.http.middlewares.nextcloud.headers.customFrameOptionsValue=SAMEORIGIN"
- "traefik.http.middlewares.nextcloud.headers.referrerPolicy=no-referrer"
- "traefik.http.middlewares.nextcloud.headers.browserXSSFilter=true"
- "traefik.http.middlewares.nextcloud.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.nextcloud_redirect.redirectregex.regex=/.well-known/(card|cal)dav"
- "traefik.http.middlewares.nextcloud_redirect.redirectregex.replacement=/remote.php/dav/"
environment:
- POSTGRES_DB=nextcloud
- POSTGRES_USER=nextcloud
- POSTGRES_PASSWORD=bajsa123
- POSTGRES_HOST=postgres
- NEXTCLOUD_ADMIN_USER=admin
- NEXTCLOUD_ADMIN_PASSWORD=bajsa123
- NEXTCLOUD_TRUSTED_DOMAINS=moln.milvert.com
- REDIS_HOST=redis
- TRUSTED_PROXIES=172.19.0.0/16
- OVERWRITECLIURL=https://moln.milvert.com
- OVERWRITEPROTOCOL=https
- OVERWRITEHOST=moln.milvert.com
volumes:
- ${DIR_LOCAL}/nextcloud:/var/www/html
- ./nextcloud/cronjob:/var/spool/cron/crontabs/www-data
- /srv/owncloud:/var/www/html/data
- /media/NAS:/media/NAS
- /mnt/gunnar:/media/gunnar
gitea:
image: gitea/gitea:1.21
container_name: gitea
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
networks:
- backend
restart: always
environment:
- USER_UID=1001
- USER_GID=1005
volumes:
#- /var/lib/gitea:/data
- ${DIR}/gitea:/data
- ./gitea/app.ini:/data/gitea/conf/app.ini
- /home/git/.ssh:/data/git/.ssh
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:2222:22"
- "3000:3000"
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.services.gitea-service.loadbalancer.server.port=3000"
- "traefik.http.routers.gitea-secure.entrypoints=web-secure"
- "traefik.http.routers.gitea-secure.rule=Host(`gitea.${DOMAIN}`)"
- "traefik.http.routers.gitea-secure.tls.certresolver=milvert_dns"
- "traefik.http.routers.gitea-secure.tls=true"
- "traefik.http.routers.gitea-secure.middlewares=chain-no-auth@file"
uptime_kuma:
image: louislam/uptime-kuma
container_name: uptime_kuma
networks:
- backend
environment:
- ADMIN_PASSWORD=${UPTIME_KUMA_PASSWORD}
- ADMIN_EMAIL=${UPTIME_KUMA_USER}
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${DIR_LOCAL}/uptime_kuma:/data
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.services.uptime-service.loadbalancer.server.port=3001"
- "traefik.http.routers.uptime-secure.entrypoints=web-secure"
- "traefik.http.routers.uptime-secure.rule=Host(`uptime.${DOMAIN}`)"
- "traefik.http.routers.uptime-secure.tls.certresolver=milvert_dns"
- "traefik.http.routers.uptime-secure.tls=true"
- "traefik.http.routers.uptime-secure.middlewares=chain-authelia@file"
healthcheck:
disable: true
######################### WEB ############################
#
# WEB
@ -472,8 +660,38 @@ services:
- "traefik.http.routers.librespeed.tls=true"
jelu:
image: wabayang/jelu
container_name: jelu
environment:
- PUID=${UUID}
- PGID=${PGID}
- TZ=${TZ}
ports:
# The HTTP port
- 11111:11111
networks:
- backend
volumes:
- ./jelu/config:/config
- ${DIR_LOCAL}/jelu/database:/database
- ${DIR_LOCAL}/jelu/files/images:/files/images
- ${DIR_LOCAL}/jelu/files/imports:/files/imports
- /etc/timezone:/etc/timezone:ro
restart: unless-stopped
labels:
- diun.enable=true
- "traefik.enable=true"
- "traefik.http.services.jelu-service.loadbalancer.server.port=11111"
- "traefik.http.routers.jelu.entrypoints=web-secure"
- "traefik.http.routers.jelu.rule=Host(`jelu.${DOMAIN}`)"
- "traefik.http.routers.jelu.middlewares=chain-no-auth@file"
- "traefik.http.routers.jelu.tls.certresolver=milvert_dns"
- "traefik.http.routers.jelu.tls=true"
vwsfriend:
image: tillsteinbach/vwsfriend:0.24.2
image: tillsteinbach/vwsfriend:0.24.7
container_name: vwfriend
ports:
- ${VWSFRIEND_PORT-4000}:${VWSFRIEND_PORT-4000}
@ -515,7 +733,7 @@ services:
- "traefik.http.routers.vwsfriend.tls=true"
vwgrafana:
image: tillsteinbach/vwsfriend-grafana:0.24.2
image: tillsteinbach/vwsfriend-grafana:0.24.5
container_name: vwgrafana
ports:
- ${GF_SERVER_HTTP_PORT-3001}:${GF_SERVER_HTTP_PORT-3000}
@ -556,6 +774,22 @@ services:
- "traefik.http.routers.vwgrafana.tls.certresolver=milvert_dns"
- "traefik.http.routers.vwgrafana.tls=true"
# weconnect_mqtt:
#image: "tillsteinbach/weconnect-mqtt:0.49.1"
#container_name: weconnect_mqtt
#restart: unless-stopped
#networks:
#backend:
#labels:
#- diun.enable=true
#environment:
#- TZ=$TZ
#- LC_ALL=sv_SE
#- USER=${WECONNECT_USER}
#- PASSWORD=${WECONNECT_PASSWORD}
#- BROKER_ADDRESS=mqtt
#- ADDITIONAL_PARAMETERS=--mqtt-username simon --mqtt-password bajsa123 --spin 9331 -vv
networks:
frontend:

View File

@ -1,8 +1,8 @@
sponsortoken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODEzODIyNTYsImlhdCI6MTY4Njc3NDI1NiwiaXNzIjoiZXZjYy5pbyIsInN1YiI6Im1pbHZlcnQifQ.HUEmc0NSPt9x5MbOHUAGU6bp3H3E3qwu6O6BAHH9FvE
sponsortoken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJldmNjLmlvIiwic3ViIjoibWlsdmVydCIsImV4cCI6MTgzMDE5MzIwMCwiaWF0IjoxNzM1NTg1MjAwLCJzcmMiOiJnaCJ9._K23QsA15DIHRjujwH8rnFZyloSw1RPIeIS4W5WLFGE
log: error
#levels:
# car: trace
levels:
tariff: trace
interval: 30s
@ -40,14 +40,20 @@ chargers:
password: X7#aEzjlEysBgl
charger: EHCNF485
circuits:
- name: main # if there is only one circuit defined the name needs to be 'main'
title: 'main circuit' # name for the UI (not implemented in UI yet)
maxCurrent: 20
maxPower: 10000
meter: my_grid # optiona
loadpoints:
- title: Garage
charger: wallbox
vehicle: car
circuit: main
mode: pv
phases: 3
mincurrent: 6
maxcurrent: 16
enable:
threshold: 0
delay: 15s
@ -73,18 +79,19 @@ meters:
energy:
source: mqtt
topic: inverter/calculated/accumulated_yield_energy
# jq: .value
timeout: 1h
timeout: 60s
currents:
- source: mqtt
topic: inverter/measure/phase_A_current
timeout: 60s
# jq: .value
- source: mqtt
topic: inverter/measure/phase_B_current
timeout: 60s
# jq: .value
- source: mqtt
topic: inverter/measure/phase_C_current
# jq: .value
timeout: 60s
- name: my_grid
type: custom
@ -94,41 +101,34 @@ meters:
- source: mqtt
topic: dsmr/reading/electricity_currently_returned
scale: -1000
timeout: 30s
- source: mqtt
topic: dsmr/reading/electricity_currently_delivered
scale: 1000
timeout: 30s
energy:
source: calc
add:
- source: mqtt
topic: dsmr/reading/electricity_returned_1
scale: 0.001
timeout: 30s
- source: mqtt
topic: dsmr/reading/electricity_delivered_1
scale: -0.001
timeout: 30s
currents:
- source: calc
add:
- source: mqtt
topic: dsmr/reading/phase_currently_delivered_l1
scale: -1
- source: mqtt
topic: dsmr/reading/phase_currently_returned_l1
- source: calc
add:
- source: mqtt
topic: dsmr/reading/phase_currently_delivered_l2
scale: -1
- source: mqtt
topic: dsmr/reading/phase_currently_returned_l2
- source: calc
add:
- source: mqtt
topic: dsmr/reading/phase_currently_delivered_l3
scale: -1
- source: mqtt
topic: dsmr/reading/phase_currently_returned_l3
- source: mqtt
topic: dsmr/reading/phase_power_current_l1
timeout: 30s
- source: mqtt
topic: dsmr/reading/phase_power_current_l2
timeout: 30s
- source: mqtt
topic: dsmr/reading/phase_power_current_l1
timeout: 30s
influx:
url: http://influx:8086

Binary file not shown.

View File

@ -1118,3 +1118,22 @@ allow_embedding = true
# Enable or disable loading other base map layers
;enable_custom_baselayers = true
[auth]
enabled = true
[auth.generic_oauth]
enabled = true
name = Authelia
icon = signin
client_id = aN0VgMKamGZvleUplkT3W7kvXJmvsWmy4C9Obd6u5XGqL7A9B7CP2xxdSIv4ljIA
client_secret = QqJ520ad7V9jX93J3NFqfWnCyklDH1UO
scopes = openid profile email groups
empty_scopes = false
auth_url = https://authelia.milvert.com/api/oidc/authorization # Replace with your own URL
token_url = https://authelia.milvert.com/api/oidc/token # Replace with your own URL
api_url = https://authelia.milvert.com/api/oidc/userinfo # Replace with your own URL
login_attribute_path = preferred_username
groups_attribute_path = groups
name_attribute_path = name
use_pkce = true
auto_login = true

View File

@ -31,21 +31,27 @@
- id: '1700687177645'
alias: Handle_motorvärmare
description: ''
trigger:
- platform: time
at: input_datetime.motorvarmare_start
condition:
triggers:
- at: input_datetime.motorvarmare_start
trigger: time
conditions:
- condition: state
entity_id: input_boolean.motorvarmare_toogle
state: 'on'
action:
- service: switch.turn_on
data: {}
actions:
- data: {}
action: switch.turn_on
target:
entity_id: switch.nodeid_22_switch
entity_id: switch.nodeid_22_nodeid_22_switch
- delay: 02:00:00
- data: {}
action: switch.turn_off
target:
entity_id: switch.nodeid_22_nodeid_22_switch
mode: single
- id: '1700693056778'
description: ''
alias: motorvärmare 2h
description: motorvärmare i 2h
trigger:
- platform: state
entity_id:
@ -62,7 +68,7 @@
entity_id: switch.nodeid_22_switch
mode: single
- id: '1703971688590'
alias: Lampa trappa
alias: Lampa trappa dag
description: ''
trigger:
- platform: state
@ -82,6 +88,53 @@
- service: light.turn_on
data:
brightness_pct: 52
kelvin: 2900
target:
device_id: ad8c90d56d6753ae960fe61560f1de66
- delay:
hours: 0
minutes: 20
seconds: 0
milliseconds: 0
- service: light.turn_off
data: {}
target:
device_id: ad8c90d56d6753ae960fe61560f1de66
mode: single
- id: '1704919949019'
alias: Update Jaffa location as MQTT location updates
description: ''
trigger:
- platform: mqtt
topic: weconnect/0/vehicles/WVGZZZE2ZPE051949/parking/parkingPosition/latitude
- platform: mqtt
topic: weconnect/0/vehicles/WVGZZZE2ZPE051949/parking/parkingPosition/longitude
condition: []
action:
- service: device_tracker.see
data:
dev_id: jaffa_location
source_type: gps
gps:
- '{{ states.sensor.none_jaffa_lat.state }}'
- '{{ states.sensor.none_jaffa_long.state }}'
initial_state: 'on'
- id: '1706989035065'
alias: Lampa trappa natt
description: ''
trigger:
- platform: state
entity_id:
- binary_sensor.h007m_occupancy
to: 'on'
condition:
- condition: time
after: '22:30:00'
before: 08:00:00
action:
- service: light.turn_on
data:
brightness_pct: 7
target:
device_id: ad8c90d56d6753ae960fe61560f1de66
- delay:
@ -94,3 +147,470 @@
target:
device_id: ad8c90d56d6753ae960fe61560f1de66
mode: single
- id: '1707166664479'
alias: 'Oscar tts '
description: 'Spelar upp en text på Oscars högtalare '
trigger:
- platform: state
entity_id:
- input_text.tts_syntesiser
condition: []
action:
- service: tts.edge_tts_say
metadata: {}
data:
cache: false
entity_id: media_player.oscar
language: sv_SE
message: '{{ states(''input_text.tts_syntesiser'') }}'
enabled: false
- service: tts.edge_tts_say
metadata: {}
data:
entity_id: media_player.oscar
message: 'Hej '
mode: single
- id: '1709494545609'
alias: växtlampa schema
trigger:
- platform: template
value_template: '{{ now().hour == 7 }}'
id: 'on'
- platform: template
value_template: '{{ (now().hour, now().minute) == (21,30) }}'
id: 'off'
action:
- service: light.turn_{{ trigger.id }}
target:
entity_id: light.vaxtlampa
- id: '1713552723716'
alias: Automation_oscar_skrivbord
description: ''
trigger:
- platform: event
event_type: button_pressed
event_data:
entity_id: switch.oscar_skrivbord
state: 'off'
id: 'off'
- platform: event
event_type: button_pressed
event_data:
entity_id: switch.oscar_skrivbord
state: 'on'
id: 'on'
condition: []
action:
- service: light.turn_{{ trigger.id }}
target:
entity_id: light.oscar_skrivbord
data: {}
- service: switch.turn_{{ trigger.id }}
metadata: {}
data: {}
target:
entity_id: switch.nodeid_13_nodeid_13_switch
mode: single
- id: '1713555224562'
alias: Automation_oscar_moln
description: ''
trigger:
- platform: event
event_type: button_pressed
event_data:
entity_id: switch.oscar_skrivbord
state: 'off'
id: 'off'
- platform: event
event_type: button_pressed
event_data:
entity_id: switch.oscar_skrivbord
state: 'on'
id: 'on'
condition: []
action:
- service: switch.turn_{{ trigger.id }}
metadata: {}
data: {}
target:
entity_id: switch.nodeid_7_nodeid_7_switch
mode: single
- id: '1715369564640'
alias: Bevattning 20 min
description: ''
trigger:
- platform: sun
event: sunrise
offset: 0
condition: []
action:
- type: turn_on
device_id: ee19c0deed59d2a266b59c30dbf7ccaa
entity_id: 068c418ccf17f83fe236590673ce7c1f
domain: switch
- delay:
hours: 0
minutes: 30
seconds: 0
milliseconds: 0
- type: turn_off
device_id: ee19c0deed59d2a266b59c30dbf7ccaa
entity_id: 068c418ccf17f83fe236590673ce7c1f
domain: switch
mode: single
- id: '1715979338256'
alias: Nattnotis
description: ''
trigger:
- platform: state
entity_id:
- scene.natt
condition:
- condition: or
conditions:
- condition: state
entity_id: binary_sensor.jaffa_locked
state: 'on'
- condition: state
entity_id: lock.h014s
state: unlocked
- condition: state
entity_id: binary_sensor.sensor_inne_tvattstuga_dt002_contact
state: 'on'
- condition: state
entity_id: binary_sensor.sensor_inne_arum_dt003_contact
state: 'on'
action:
- service: notify.mobile_app_simon_mobil
metadata: {}
data:
title: Kolla lås.
message: "**Natt-scen aktiverad!** * Jaffadörren: {{ states('binary_sensor.jaffa_locked').state
}} * Tvättstuga: {{ states('binary_sensor.sensor_inne_tvattstuga_dt002_contact').state
}} * Arbetsrum: {{ states('binary_sensor.sensor_inne_arum_dt003_contact').state
}} \n"
mode: single
- id: '1723749734599'
alias: Kväll_on_off
description: ''
triggers:
- event: sunset
offset: 00:30:00
trigger: sun
- at: '19:30:00'
id: time_on
trigger: time
- at: '23:15:00'
id: time_off
trigger: time
conditions: []
actions:
- data:
entity_id: '{{ ''scene.kvalls_belysning'' if now().hour < 23 else ''scene.natt''
}}
'
action: scene.turn_on
- id: '1723751411352'
alias: ute_av_på
description: ''
triggers:
- event: sunrise
offset: -00:15:00
trigger: sun
- event: sunset
offset: 00:15:00
trigger: sun
- at: 07:55:00
trigger: time
conditions: []
actions:
- data:
entity_id: '{{ ''scene.ute'' if trigger.event == ''sunset'' else ''scene.ute_av''
}}
'
action: scene.turn_on
mode: single
- id: '1725646259613'
alias: Kök på 5 min
description: ''
trigger:
- platform: state
entity_id:
- light.kok_ct
to: 'on'
condition: []
action:
- entity_id: script.kok_ct_timer_2
action: script.turn_on
mode: restart
- id: '1729969302596'
alias: Tänd och släck vrum upp vid helgmorgon
description: Tänder lampan kl 06:00 och släcker den kl 07:30 på lördagar och söndagar
triggers:
- at: 06:00:00
trigger: time
- at: 07:30:00
trigger: time
conditions:
- condition: time
weekday:
- sat
- sun
actions:
- choose:
- conditions:
- condition: template
value_template: '{{ trigger.platform == ''time'' and trigger.now.strftime(''%H:%M:%S'')
== ''06:00:00'' }}'
sequence:
- data:
brightness_pct: 9
target:
device_id: 79ba72943d4ed67fa5dc4fdbfe4fa54d
action: light.turn_on
- conditions:
- condition: template
value_template: '{{ trigger.platform == ''time'' and trigger.now.strftime(''%H:%M:%S'')
== ''07:30:00'' }}'
sequence:
- target:
device_id: 79ba72943d4ed67fa5dc4fdbfe4fa54d
action: light.turn_off
data: {}
- action: switch.turn_off
metadata: {}
data: {}
target:
device_id: d93a6c62e11bec4c4d480497363d4512
mode: single
- id: '1731354729078'
alias: Tänd garage
description: ''
triggers:
- type: occupied
device_id: 0c06d34c097db550f6339bdf16b8b408
entity_id: c03b9e6b324f34b4ff4dc523b49ed991
domain: binary_sensor
trigger: device
- trigger: state
entity_id:
- binary_sensor.sensor_inne_garage_m001_occupancy
to: 'on'
conditions: []
actions:
- action: light.turn_on
metadata: {}
data: {}
target:
device_id: f5781cfa34b2e0a238f6f6333e7d7fa2
- action: switch.turn_on
metadata: {}
data: {}
target:
entity_id: switch.nodeid_16_nodeid_16_switch
- delay:
minutes: 5
- action: light.turn_off
metadata: {}
data: {}
target:
device_id: f5781cfa34b2e0a238f6f6333e7d7fa2
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id: switch.nodeid_16_nodeid_16_switch
mode: single
- id: '1731870335988'
alias: Lampa, byt slinga mot fasad
description: ''
triggers:
- at: '23:15:00'
trigger: time
- at: 06:00:00
trigger: time
conditions: []
actions:
- choose:
- conditions:
- condition: time
before: 06:00:00
sequence:
- action: switch.turn_off
data: {}
target:
entity_id: switch.h024s
- target:
entity_id: switch.h015l
action: switch.turn_on
data: {}
- conditions:
- condition: time
after: 05:59:59
sequence:
- action: switch.toggle
data: {}
target:
entity_id: switch.h024s
- target:
entity_id: switch.h015l
action: switch.toggle
data: {}
mode: single
- id: '1733170240917'
alias: 'Jul belysning '
description: ''
triggers:
- value_template: '{{ now().hour == 16 }}'
id: 'on'
trigger: template
- value_template: '{{ (now().hour) == 8 }}'
id: 'off'
trigger: template
actions:
- target:
entity_id: group.jul_group
action: homeassistant.turn_{{ trigger.id }}
- id: '1735463627194'
alias: Ada släck 0800
description: ''
triggers:
- trigger: time
at: 08:00:00
conditions: []
actions:
- action: light.turn_off
metadata: {}
data: {}
target:
entity_id: light.ada_jordglob2
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id: switch.ada_slnga
- action: light.turn_off
metadata: {}
data: {}
target:
device_id: 069299a3de0a369de40dd512292a2828
mode: single
- id: '1740430707464'
alias: Sov gott Oscar när sleep timer är 0
description: ''
triggers:
- entity_id: sensor.sonos_sleep_timer
to: '0'
trigger: state
actions:
- action: script.talk_on_oscar
data:
message: Sov Gott Oscar! Dags att sova
mode: single
- id: '1741036830396'
alias: 'Reser temp '
description: ''
triggers:
- trigger: time
at: 06:00:00
conditions: []
actions:
- action: climate.set_fan_mode
metadata: {}
data:
fan_mode: '3'
target:
device_id: ced8502d2ee4ac70dbb9929329ec3ae2
- action: climate.set_temperature
metadata: {}
data:
temperature: 22
target:
device_id: ced8502d2ee4ac70dbb9929329ec3ae2
mode: single
- id: '1743450529687'
alias: Styr temperaturen vrum
description: ''
triggers:
- type: opened
device_id: c57486d3c2297020f9b19f7128bf867e
entity_id: 19d29462d89383fd11e32861269de77d
domain: binary_sensor
metadata:
secondary: false
trigger: device
for:
hours: 0
minutes: 0
seconds: 30
- type: not_opened
device_id: c57486d3c2297020f9b19f7128bf867e
entity_id: 19d29462d89383fd11e32861269de77d
domain: binary_sensor
trigger: device
conditions: []
actions:
- choose:
- conditions:
- type: is_open
condition: device
device_id: c57486d3c2297020f9b19f7128bf867e
entity_id: 19d29462d89383fd11e32861269de77d
domain: binary_sensor
for:
hours: 0
minutes: 0
seconds: 30
sequence:
- data:
value: '{{ state_attr(''climate.vardagsrum_2'', ''temperature'') }}'
target:
entity_id: input_number.vardagsrum_temperatur_sparad
action: input_number.set_value
- metadata: {}
data:
temperature: 6
target:
entity_id: climate.vardagsrum_2
action: climate.set_temperature
- conditions:
- type: is_not_open
condition: device
device_id: c57486d3c2297020f9b19f7128bf867e
entity_id: 19d29462d89383fd11e32861269de77d
domain: binary_sensor
sequence:
- metadata: {}
data:
temperature: '{{ states(''input_number.vardagsrum_temperatur_sparad'') }}'
target:
entity_id: climate.vardagsrum_2
action: climate.set_temperature
mode: single
- id: '1744573491574'
alias: Möjlig frost - baserat på trend eller prognos
triggers:
- at: '20:00:00'
trigger: time
conditions:
- condition: or
conditions:
- condition: template
value_template: "{% set forecast = state_attr('weather.forecast_home', 'forecast')
%} {% if forecast %}\n {{ forecast[0].templow | float < 0 }}\n{% else %}\n
\ false\n{% endif %}\n"
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.h017s_temperature
below: 4
- condition: numeric_state
entity_id: sensor.h017s_derivata
below: -0.5
actions:
- data:
message: Varning! Frost kan vara på gång ❄️ (enligt prognos eller temptrend)
action: notify.mobile_app_simon_mobil
mode: single

View File

@ -5,23 +5,67 @@ default_config:
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
extra_module_url:
- /config/www/community/lovelace-card-mod/card-mod.js
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
command_line: !include sensor.yaml
battery_notes:
#lovelace:
# mode: storage
# resources:
# - url: /local/week-planner-card_2.js
# type: module
# dashboards:
# dash-general:
# mode: yaml
# filename: dashboards/default.yaml
# title: Overview
# icon: mdi:tools
# show_in_sidebar: true
# require_admin: false
tts:
- platform: edge_tts
service-name: edge-say
language: sv-SE
#This is to synthesise TTS for the Google Home Mini
input_text:
tts_syntesiser:
name: TTS-til-Google
proximity:
home_jaffa:
zone: home
devices:
- device_tracker.jaffa_location
tolerance: 5
unit_of_measurement: km
homeassistant:
internal_url: http://10.0.0.203:8123
external_url: https://ha.milvert.com
auth_providers:
- type: homeassistant
- type: legacy_api_password
api_password: !secret http_password
packages: !include_dir_named packages
#evcc: !include packages/evcc.yaml
allowlist_external_dirs:
- "/config/files"
customize:
# Add an entry for each entity that you want to overwrite.
thermostat.family_room:
entity_picture: https://example.com/images/nest.jpg
friendly_name: Nest
sonos:
media_player:
advertise_addr: 10.0.0.203
@ -29,6 +73,19 @@ sonos:
- 10.0.3.33
- 10.0.3.32
logger:
default: error
#logs:
#homeassistant.components.command_line: debug
#adax: debug
#custom_components.adax: debug
#homeassistant.components.adax: debug
# rflink: error
# homeassistant.components.rflink: debug
http:
use_x_forwarded_for: true
@ -37,19 +94,20 @@ http:
trusted_proxies:
- 10.0.0.223
- 172.19.0.0/24
panel_iframe:
configurator:
title: Configurator
icon: mdi:wrench
url: http://10.0.0.3:3218
require_admin: true
zwave:
title: zwave
icon: mdi:wrench
url: http://10.0.0.3:8091
require_admin: true
template:
- sensor:
- name: "Vardagsrum Source"
state: >
{% set source = state_attr('media_player.vardagsrum', 'source') | trim %}
{% if source == "TV" %}
TV
{% else %}
{{ source if source else "none" }}
{% endif %}
attributes:
original_source: "{{ state_attr('media_player.vardagsrum', 'source') }}"

View File

@ -1,80 +1,58 @@
"""
HACS gives you a powerful UI to handle downloads of all your custom needs.
"""HACS gives you a powerful UI to handle downloads of all your custom needs.
For more details about this integration, please refer to the documentation at
https://hacs.xyz/
"""
from __future__ import annotations
import os
from typing import Any
from __future__ import annotations
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
from aiogithubapi.const import ACCEPT_HEADERS
from awesomeversion import AwesomeVersion
from homeassistant.components.frontend import async_remove_panel
from homeassistant.components.lovelace.system_health import system_health_info
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.start import async_at_start
from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase
from .const import DOMAIN, MINIMUM_HA_VERSION, STARTUP
from .const import DOMAIN, HACS_SYSTEM_ID, MINIMUM_HA_VERSION, STARTUP
from .data_client import HacsDataClient
from .enums import ConfigurationType, HacsDisabledReason, HacsStage, LovelaceMode
from .enums import HacsDisabledReason, HacsStage, LovelaceMode
from .frontend import async_register_frontend
from .utils.configuration_schema import hacs_config_combined
from .utils.data import HacsData
from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager
from .utils.version import version_left_higher_or_equal_then_right
from .websocket import async_register_websocket_commands
CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA)
PLATFORMS = [Platform.SWITCH, Platform.UPDATE]
async def async_initialize_integration(
async def _async_initialize_integration(
hass: HomeAssistant,
*,
config_entry: ConfigEntry | None = None,
config: dict[str, Any] | None = None,
config_entry: ConfigEntry,
) -> bool:
"""Initialize the integration"""
hass.data[DOMAIN] = hacs = HacsBase()
hacs.enable_hacs()
if config is not None:
if DOMAIN not in config:
return True
if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY:
return True
hacs.configuration.update_from_dict(
{
"config_type": ConfigurationType.YAML,
**config[DOMAIN],
"config": config[DOMAIN],
}
)
if config_entry.source == SOURCE_IMPORT:
# Import is not supported
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
if config_entry is not None:
if config_entry.source == SOURCE_IMPORT:
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
hacs.configuration.update_from_dict(
{
"config_entry": config_entry,
"config_type": ConfigurationType.CONFIG_ENTRY,
**config_entry.data,
**config_entry.options,
}
)
hacs.configuration.update_from_dict(
{
"config_entry": config_entry,
**config_entry.data,
**config_entry.options,
},
)
integration = await async_get_integration(hass, DOMAIN)
@ -104,7 +82,6 @@ async def async_initialize_integration(
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
# If this happens, the users YAML is not valid, we assume YAML mode
pass
hacs.log.debug("Configuration type: %s", hacs.configuration.config_type)
hacs.core.config_path = hacs.hass.config.path()
if hacs.core.ha_version is None:
@ -131,19 +108,18 @@ async def async_initialize_integration(
"""HACS startup tasks."""
hacs.enable_hacs()
for location in (
hass.config.path("custom_components/custom_updater.py"),
hass.config.path("custom_components/custom_updater/__init__.py"),
):
if os.path.exists(location):
hacs.log.critical(
"This cannot be used with custom_updater. "
"To use this you need to remove custom_updater form %s",
location,
)
try:
import custom_components.custom_updater
except ImportError:
pass
else:
hacs.log.critical(
"HACS cannot be used with custom_updater. "
"To use HACS you need to remove custom_updater from `custom_components`",
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not version_left_higher_or_equal_then_right(
hacs.core.ha_version.string,
@ -160,39 +136,23 @@ async def async_initialize_integration(
hacs.disable_hacs(HacsDisabledReason.RESTORE)
return False
if not hacs.configuration.experimental:
can_update = await hacs.async_can_update()
hacs.log.debug("Can update %s repositories", can_update)
hacs.set_active_categories()
async_register_websocket_commands(hass)
async_register_frontend(hass, hacs)
await async_register_frontend(hass, hacs)
if hacs.configuration.config_type == ConfigurationType.YAML:
hass.async_create_task(
async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, hacs.configuration.config)
)
hacs.log.info("Update entities are only supported when using UI configuration")
else:
await hass.config_entries.async_forward_entry_setups(
config_entry,
[Platform.SENSOR, Platform.UPDATE]
if hacs.configuration.experimental
else [Platform.SENSOR],
)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hacs.set_stage(HacsStage.SETUP)
if hacs.system.disabled:
return False
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
hacs.set_stage(HacsStage.WAITING)
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
return not hacs.system.disabled
async def async_try_startup(_=None):
@ -202,10 +162,7 @@ async def async_initialize_integration(
except AIOGitHubAPIException:
startup_result = False
if not startup_result:
if (
hacs.configuration.config_type == ConfigurationType.YAML
or hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN
):
if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN:
hacs.log.info("Could not setup HACS, trying again in 15 min")
async_call_later(hass, 900, async_try_startup)
return
@ -213,37 +170,19 @@ async def async_initialize_integration(
await async_try_startup()
# Remove old (v0-v1) sensor if it exists, can be removed in v3
er = async_get_entity_registry(hass)
if old_sensor := er.async_get_entity_id("sensor", DOMAIN, HACS_SYSTEM_ID):
er.async_remove(old_sensor)
# Mischief managed!
return True
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up this integration using yaml."""
if DOMAIN in config:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_configuration",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_configuration",
learn_more_url="https://hacs.xyz/docs/configuration/options",
)
LOGGER.warning(
"YAML configuration of HACS is deprecated and will be "
"removed in version 2.0.0, there will be no automatic "
"import of this. "
"Please remove it from your configuration, "
"restart Home Assistant and use the UI to configure it instead."
)
return await async_initialize_integration(hass=hass, config=config)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
setup_result = await async_initialize_integration(hass=hass, config_entry=config_entry)
setup_result = await _async_initialize_integration(hass=hass, config_entry=config_entry)
hacs: HacsBase = hass.data[DOMAIN]
return setup_result and not hacs.system.disabled
@ -259,7 +198,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
# Clear out pending queue
hacs.queue.clear()
for task in hacs.recuring_tasks:
for task in hacs.recurring_tasks:
# Cancel all pending tasks
task()
@ -269,15 +208,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
try:
if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel")
hass.components.frontend.async_remove_panel("hacs")
async_remove_panel(hass, "hacs")
except AttributeError:
pass
platforms = ["sensor"]
if hacs.configuration.experimental:
platforms.append("update")
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, platforms)
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
hacs.set_stage(None)
hacs.disable_hacs(HacsDisabledReason.REMOVED)

View File

@ -1,16 +1,17 @@
"""Base HACS class."""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field
from datetime import timedelta
import gzip
import logging
import math
import os
import pathlib
import shutil
from typing import TYPE_CHECKING, Any, Awaitable, Callable
from typing import TYPE_CHECKING, Any
from aiogithubapi import (
AIOGitHubAPIException,
@ -24,23 +25,22 @@ from aiogithubapi import (
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
from aiohttp.client import ClientSession, ClientTimeout
from awesomeversion import AwesomeVersion
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.components.persistent_notification import (
async_create as async_create_persistent_notification,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.loader import Integration
from homeassistant.util import dt
from custom_components.hacs.repositories.base import (
HACS_MANIFEST_KEYS_TO_EXPORT,
REPOSITORY_KEYS_TO_EXPORT,
)
from .const import DOMAIN, TV, URL_BASE
from .coordinator import HacsUpdateCoordinator
from .data_client import HacsDataClient
from .enums import (
ConfigurationType,
HacsCategory,
HacsDisabledReason,
HacsDispatchEvent,
@ -58,12 +58,14 @@ from .exceptions import (
HacsRepositoryExistException,
HomeAssistantCoreRepositoryException,
)
from .repositories import RERPOSITORY_CLASSES
from .utils.decode import decode_content
from .repositories import REPOSITORY_CLASSES
from .repositories.base import HACS_MANIFEST_KEYS_TO_EXPORT, REPOSITORY_KEYS_TO_EXPORT
from .utils.file_system import async_exists
from .utils.json import json_loads
from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager
from .utils.store import async_load_from_store, async_save_to_store
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING:
from .repositories.base import HacsRepository
@ -113,15 +115,11 @@ class HacsConfiguration:
appdaemon: bool = False
config: dict[str, Any] = field(default_factory=dict)
config_entry: ConfigEntry | None = None
config_type: ConfigurationType | None = None
country: str = "ALL"
debug: bool = False
dev: bool = False
experimental: bool = False
frontend_repo_url: str = ""
frontend_repo: str = ""
netdaemon_path: str = "netdaemon/apps/"
netdaemon: bool = False
plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/"
python_script: bool = False
@ -142,6 +140,8 @@ class HacsConfiguration:
raise HacsException("Configuration is not valid.")
for key in data:
if key in {"experimental", "netdaemon", "release_limit", "debug"}:
continue
self.__setattr__(key, data[key])
@ -217,6 +217,13 @@ class HacsRepositories:
"""Return a list of downloaded repositories."""
return [repo for repo in self._repositories if repo.data.installed]
def category_downloaded(self, category: HacsCategory) -> bool:
"""Check if a given category has been downloaded."""
for repository in self.list_downloaded:
if repository.data.category == category:
return True
return False
def register(self, repository: HacsRepository, default: bool = False) -> None:
"""Register a repository."""
repo_id = str(repository.data.id)
@ -348,9 +355,6 @@ class HacsRepositories:
class HacsBase:
"""Base HACS class."""
common = HacsCommon()
configuration = HacsConfiguration()
core = HacsCore()
data: HacsData | None = None
data_client: HacsDataClient | None = None
frontend_version: str | None = None
@ -358,17 +362,24 @@ class HacsBase:
githubapi: GitHubAPI | None = None
hass: HomeAssistant | None = None
integration: Integration | None = None
log: logging.Logger = LOGGER
queue: QueueManager | None = None
recuring_tasks = []
repositories: HacsRepositories = HacsRepositories()
repository: AIOGitHubAPIRepository | None = None
session: ClientSession | None = None
stage: HacsStage | None = None
status = HacsStatus()
system = HacsSystem()
validation: ValidationManager | None = None
version: str | None = None
version: AwesomeVersion | None = None
def __init__(self) -> None:
"""Initialize."""
self.common = HacsCommon()
self.configuration = HacsConfiguration()
self.coordinators: dict[HacsCategory, HacsUpdateCoordinator] = {}
self.core = HacsCore()
self.log = LOGGER
self.recurring_tasks: list[Callable[[], None]] = []
self.repositories = HacsRepositories()
self.status = HacsStatus()
self.system = HacsSystem()
@property
def integration_dir(self) -> pathlib.Path:
@ -394,12 +405,7 @@ class HacsBase:
if reason != HacsDisabledReason.REMOVED:
self.log.error("HACS is disabled - %s", reason)
if (
reason == HacsDisabledReason.INVALID_TOKEN
and self.configuration.config_type == ConfigurationType.CONFIG_ENTRY
):
self.configuration.config_entry.state = ConfigEntryState.SETUP_ERROR
self.configuration.config_entry.reason = "Authentication failed"
if reason == HacsDisabledReason.INVALID_TOKEN:
self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass)
def enable_hacs(self) -> None:
@ -413,12 +419,14 @@ class HacsBase:
if category not in self.common.categories:
self.log.info("Enable category: %s", category)
self.common.categories.add(category)
self.coordinators[category] = HacsUpdateCoordinator()
def disable_hacs_category(self, category: HacsCategory) -> None:
"""Disable HACS category."""
if category in self.common.categories:
self.log.info("Disabling category: %s", category)
self.common.categories.pop(category)
self.coordinators.pop(category)
async def async_save_file(self, file_path: str, content: Any) -> bool:
"""Save a file."""
@ -451,12 +459,13 @@ class HacsBase:
try:
await self.hass.async_add_executor_job(_write_file)
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as error:
self.log.error("Could not write data to %s - %s", file_path, error)
return False
return os.path.exists(file_path)
return await async_exists(self.hass, file_path)
async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for."""
@ -472,24 +481,13 @@ class HacsBase:
)
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.log.exception(exception)
return 0
async def async_github_get_hacs_default_file(self, filename: str) -> list:
"""Get the content of a default file."""
response = await self.async_github_api_method(
method=self.githubapi.repos.contents.get,
repository=HacsGitHubRepo.DEFAULT,
path=filename,
)
if response is None:
return []
return json_loads(decode_content(response.data.content))
async def async_github_api_method(
self,
method: Callable[[], Awaitable[TV]],
@ -513,7 +511,8 @@ class HacsBase:
except GitHubException as exception:
_exception = exception
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.log.exception(exception)
_exception = exception
@ -545,7 +544,7 @@ class HacsBase:
):
raise AddonRepositoryException()
if category not in RERPOSITORY_CLASSES:
if category not in REPOSITORY_CLASSES:
self.log.warning(
"%s is not a valid repository category, %s will not be registered.",
category,
@ -556,7 +555,7 @@ class HacsBase:
if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None:
repository_full_name = renamed
repository: HacsRepository = RERPOSITORY_CLASSES[category](self, repository_full_name)
repository: HacsRepository = REPOSITORY_CLASSES[category](self, repository_full_name)
if check:
try:
await repository.async_registration(ref)
@ -566,7 +565,8 @@ class HacsBase:
self.log.error("Validation for %s failed.", repository_full_name)
if self.system.action:
raise HacsException(
f"::error:: Validation for {repository_full_name} failed."
f"::error:: Validation for {
repository_full_name} failed."
)
return repository.validate.errors
if self.system.action:
@ -582,7 +582,8 @@ class HacsBase:
except AIOGitHubAPIException as exception:
self.common.skip.add(repository.data.full_name)
raise HacsException(
f"Validation for {repository_full_name} failed with {exception}."
f"Validation for {
repository_full_name} failed with {exception}."
) from exception
if self.status.new:
@ -592,7 +593,7 @@ class HacsBase:
repository.data.id = repository_id
else:
if self.hass is not None and ((check and repository.data.new) or self.status.new):
if self.hass is not None and check and repository.data.new:
self.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
@ -613,91 +614,84 @@ class HacsBase:
for repo in critical:
if not repo["acknowledged"]:
self.log.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create(
title="URGENT!", message="**Check the HACS panel!**"
async_create_persistent_notification(
self.hass, title="URGENT!", message="**Check the HACS panel!**"
)
break
if not self.configuration.experimental:
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_downloaded_repositories, timedelta(hours=48)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_all_repositories,
timedelta(hours=96),
)
)
else:
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_load_hacs_from_github,
timedelta(hours=48),
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_downloaded_custom_repositories, timedelta(hours=48)
self.recurring_tasks.append(
async_track_time_interval(
self.hass,
self.async_load_hacs_from_github,
timedelta(hours=48),
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_get_all_category_repositories, timedelta(hours=6)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_update_downloaded_custom_repositories, timedelta(hours=48)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_check_rate_limit, timedelta(minutes=5)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_prosess_queue, timedelta(minutes=10)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_get_all_category_repositories, timedelta(hours=6)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_handle_critical_repositories, timedelta(hours=6)
self.recurring_tasks.append(
async_track_time_interval(self.hass, self.async_check_rate_limit, timedelta(minutes=5))
)
self.recurring_tasks.append(
async_track_time_interval(self.hass, self.async_process_queue, timedelta(minutes=10))
)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_handle_critical_repositories, timedelta(hours=6)
)
)
self.hass.bus.async_listen_once(
unsub = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write
)
if config_entry := self.configuration.config_entry:
config_entry.async_on_unload(unsub)
self.log.debug("There are %s scheduled recurring tasks", len(self.recuring_tasks))
self.log.debug("There are %s scheduled recurring tasks", len(self.recurring_tasks))
self.status.startup = False
self.async_dispatch(HacsDispatchEvent.STATUS, {})
await self.async_handle_removed_repositories()
await self.async_get_all_category_repositories()
await self.async_update_downloaded_repositories()
self.set_stage(HacsStage.RUNNING)
self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
await self.async_handle_critical_repositories()
await self.async_prosess_queue()
await self.async_process_queue()
self.async_dispatch(HacsDispatchEvent.STATUS, {})
async def async_download_file(self, url: str, *, headers: dict | None = None) -> bytes | None:
async def async_download_file(
self,
url: str,
*,
headers: dict | None = None,
keep_url: bool = False,
nolog: bool = False,
**_,
) -> bytes | None:
"""Download files, and return the content."""
if url is None:
return None
if "tags/" in url:
if not keep_url and "tags/" in url:
url = url.replace("tags/", "")
self.log.debug("Downloading %s", url)
self.log.debug("Trying to download %s", url)
timeouts = 0
while timeouts < 5:
@ -713,9 +707,10 @@ class HacsBase:
return await request.read()
raise HacsException(
f"Got status code {request.status} when trying to download {url}"
f"Got status code {
request.status} when trying to download {url}"
)
except asyncio.TimeoutError:
except TimeoutError:
self.log.warning(
"A timeout of 60! seconds was encountered while downloading %s, "
"using over 60 seconds to download a single file is not normal. "
@ -731,23 +726,34 @@ class HacsBase:
continue
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.log.exception("Download failed - %s", exception)
if not nolog:
self.log.exception("Download failed - %s", exception)
return None
async def async_recreate_entities(self) -> None:
"""Recreate entities."""
if self.configuration == ConfigurationType.YAML or not self.configuration.experimental:
return
platforms = [Platform.UPDATE]
platforms = [Platform.SENSOR, Platform.UPDATE]
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
# Workaround for core versions without https://github.com/home-assistant/core/pull/117084
if self.core.ha_version < AwesomeVersion("2024.6.0"):
unload_platforms_lock = asyncio.Lock()
async with unload_platforms_lock:
on_unload = self.configuration.config_entry._on_unload
self.configuration.config_entry._on_unload = []
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
self.configuration.config_entry._on_unload = on_unload
else:
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
await self.hass.config_entries.async_forward_entry_setups(
self.configuration.config_entry, platforms
)
@ -760,49 +766,40 @@ class HacsBase:
def set_active_categories(self) -> None:
"""Set the active categories."""
self.common.categories = set()
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN):
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN, HacsCategory.TEMPLATE):
self.enable_hacs_category(HacsCategory(category))
if self.configuration.experimental and self.core.ha_version >= "2023.4.0b0":
self.enable_hacs_category(HacsCategory.TEMPLATE)
if HacsCategory.PYTHON_SCRIPT in self.hass.config.components:
if (
HacsCategory.PYTHON_SCRIPT in self.hass.config.components
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
):
self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT)
if self.hass.services.has_service("frontend", "reload_themes"):
if self.hass.services.has_service(
"frontend", "reload_themes"
) or self.repositories.category_downloaded(HacsCategory.THEME):
self.enable_hacs_category(HacsCategory.THEME)
if self.configuration.appdaemon:
self.enable_hacs_category(HacsCategory.APPDAEMON)
if self.configuration.netdaemon:
downloaded_netdaemon = [
x
for x in self.repositories.list_downloaded
if x.data.category == HacsCategory.NETDAEMON
]
if len(downloaded_netdaemon) != 0:
self.log.warning(
"NetDaemon in HACS is deprectaded. It will stop working in the future. "
"Please remove all your current NetDaemon repositories from HACS "
"and download them manually if you want to continue using them."
)
self.enable_hacs_category(HacsCategory.NETDAEMON)
async def async_load_hacs_from_github(self, _=None) -> None:
"""Load HACS from GitHub."""
if self.configuration.experimental and self.status.inital_fetch_done:
if self.status.inital_fetch_done:
return
try:
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
should_recreate_entities = False
if repository is None:
should_recreate_entities = True
await self.async_register_repository(
repository_full_name=HacsGitHubRepo.INTEGRATION,
category=HacsCategory.INTEGRATION,
default=True,
)
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
elif self.configuration.experimental and not self.status.startup:
elif not self.status.startup:
self.log.error("Scheduling update of hacs/integration")
self.queue.add(repository.common_update())
if repository is None:
@ -813,6 +810,9 @@ class HacsBase:
repository.data.new = False
repository.data.releases = True
if should_recreate_entities:
await self.async_recreate_entities()
self.repository = repository.repository_object
self.repositories.mark_default(repository)
except HacsException as exception:
@ -832,8 +832,6 @@ class HacsBase:
await asyncio.gather(
*[
self.async_get_category_repositories_experimental(category)
if self.configuration.experimental
else self.async_get_category_repositories(HacsCategory(category))
for category in self.common.categories or []
]
)
@ -842,7 +840,7 @@ class HacsBase:
"""Update all category repositories."""
self.log.debug("Fetching updated content for %s", category)
try:
category_data = await self.data_client.get_data(category)
category_data = await self.data_client.get_data(category, validate=True)
except HacsNotModifiedException:
self.log.debug("No updates for %s", category)
return
@ -853,14 +851,14 @@ class HacsBase:
await self.data.register_unknown_repositories(category_data, category)
for repo_id, repo_data in category_data.items():
repo = repo_data["full_name"]
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if self.repositories.is_removed(repo):
repo_name = repo_data["full_name"]
if self.common.renamed_repositories.get(repo_name):
repo_name = self.common.renamed_repositories[repo_name]
if self.repositories.is_removed(repo_name):
continue
if repo in self.common.archived_repositories:
if repo_name in self.common.archived_repositories:
continue
if repository := self.repositories.get_by_full_name(repo):
if repository := self.repositories.get_by_full_name(repo_name):
self.repositories.set_repository_id(repository, repo_id)
self.repositories.mark_default(repository)
if repository.data.last_fetched is None or (
@ -871,15 +869,6 @@ class HacsBase:
repository.repository_manifest.update_data(
{**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest}
)
self.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
"id": 1337,
"action": "update",
"repository": repository.data.full_name,
"repository_id": repository.data.id,
},
)
if category == "integration":
self.status.inital_fetch_done = True
@ -896,50 +885,8 @@ class HacsBase:
)
self.repositories.unregister(repository)
async def async_get_category_repositories(self, category: HacsCategory) -> None:
"""Get repositories from category."""
if self.system.disabled:
return
try:
repositories = await self.async_github_get_hacs_default_file(category)
except HacsException:
return
for repo in repositories:
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if self.repositories.is_removed(repo):
continue
if repo in self.common.archived_repositories:
continue
repository = self.repositories.get_by_full_name(repo)
if repository is not None:
self.repositories.mark_default(repository)
if self.status.new and self.configuration.dev:
# Force update for new installations
self.queue.add(repository.common_update())
continue
self.queue.add(
self.async_register_repository(
repository_full_name=repo,
category=category,
default=True,
)
)
async def async_update_all_repositories(self, _=None) -> None:
"""Update all repositories."""
if self.system.disabled:
return
self.log.debug("Starting recurring background task for all repositories")
for repository in self.repositories.list_all:
if repository.data.category in self.common.categories:
self.queue.add(repository.common_update())
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {"action": "reload"})
self.log.debug("Recurring background task for all repositories done")
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
self.coordinators[category].async_update_listeners()
async def async_check_rate_limit(self, _=None) -> None:
"""Check rate limit."""
@ -951,9 +898,9 @@ class HacsBase:
self.log.debug("Ratelimit indicate we can update %s", can_update)
if can_update > 0:
self.enable_hacs()
await self.async_prosess_queue()
await self.async_process_queue()
async def async_prosess_queue(self, _=None) -> None:
async def async_process_queue(self, _=None) -> None:
"""Process the queue."""
if self.system.disabled:
self.log.debug("HACS is disabled")
@ -993,12 +940,7 @@ class HacsBase:
self.log.info("Loading removed repositories")
try:
if self.configuration.experimental:
removed_repositories = await self.data_client.get_data("removed")
else:
removed_repositories = await self.async_github_get_hacs_default_file(
HacsCategory.REMOVED
)
removed_repositories = await self.data_client.get_data("removed", validate=True)
except HacsException:
return
@ -1013,21 +955,20 @@ class HacsBase:
continue
if repository.data.installed:
if removed.removal_type != "critical":
if self.configuration.experimental:
async_create_issue(
hass=self.hass,
domain=DOMAIN,
issue_id=f"removed_{repository.data.id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="removed",
translation_placeholders={
"name": repository.data.full_name,
"reason": removed.reason,
"repositry_id": repository.data.id,
},
)
async_create_issue(
hass=self.hass,
domain=DOMAIN,
issue_id=f"removed_{repository.data.id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="removed",
translation_placeholders={
"name": repository.data.full_name,
"reason": removed.reason,
"repositry_id": repository.data.id,
},
)
self.log.warning(
"You have '%s' installed with HACS "
"this repository has been removed from HACS, please consider removing it. "
@ -1042,30 +983,43 @@ class HacsBase:
if need_to_save:
await self.data.async_write()
async def async_update_downloaded_repositories(self, _=None) -> None:
"""Execute the task."""
if self.system.disabled or self.configuration.experimental:
return
self.log.info("Starting recurring background task for downloaded repositories")
for repository in self.repositories.list_downloaded:
if repository.data.category in self.common.categories:
self.queue.add(repository.update_repository(ignore_issues=True))
self.log.debug("Recurring background task for downloaded repositories done")
async def async_update_downloaded_custom_repositories(self, _=None) -> None:
"""Execute the task."""
if self.system.disabled or not self.configuration.experimental:
if self.system.disabled:
return
self.log.info("Starting recurring background task for downloaded custom repositories")
repositories_to_update = 0
repositories_updated = asyncio.Event()
async def update_repository(repository: HacsRepository) -> None:
"""Update a repository"""
nonlocal repositories_to_update
await repository.update_repository(ignore_issues=True)
repositories_to_update -= 1
if not repositories_to_update:
repositories_updated.set()
for repository in self.repositories.list_downloaded:
if (
repository.data.category in self.common.categories
and not self.repositories.is_default(repository.data.id)
):
self.queue.add(repository.update_repository(ignore_issues=True))
repositories_to_update += 1
self.queue.add(update_repository(repository))
async def update_coordinators() -> None:
"""Update all coordinators."""
await repositories_updated.wait()
for coordinator in self.coordinators.values():
coordinator.async_update_listeners()
if config_entry := self.configuration.config_entry:
config_entry.async_create_background_task(
self.hass, update_coordinators(), "update_coordinators"
)
else:
self.hass.async_create_background_task(update_coordinators(), "update_coordinators")
self.log.debug("Recurring background task for downloaded custom repositories done")
@ -1077,10 +1031,7 @@ class HacsBase:
was_installed = False
try:
if self.configuration.experimental:
critical = await self.data_client.get_data("critical")
else:
critical = await self.async_github_get_hacs_default_file("critical")
critical = await self.data_client.get_data("critical", validate=True)
except (GitHubNotModifiedException, HacsNotModifiedException):
return
except HacsException:
@ -1134,11 +1085,10 @@ class HacsBase:
self.log.critical("Restarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))
@callback
def async_setup_frontend_endpoint_plugin(self) -> None:
async def async_setup_frontend_endpoint_plugin(self) -> None:
"""Setup the http endpoints for plugins if its not already handled."""
if self.status.active_frontend_endpoint_plugin or not os.path.exists(
self.hass.config.path("www/community")
if self.status.active_frontend_endpoint_plugin or not await async_exists(
self.hass, self.hass.config.path("www/community")
):
return
@ -1150,26 +1100,11 @@ class HacsBase:
use_cache,
)
self.hass.http.register_static_path(
await async_register_static_path(
self.hass,
URL_BASE,
self.hass.config.path("www/community"),
cache_headers=use_cache,
)
self.status.active_frontend_endpoint_plugin = True
@callback
def async_setup_frontend_endpoint_themes(self) -> None:
"""Setup the http endpoints for themes if its not already handled."""
if (
self.configuration.experimental
or self.status.active_frontend_endpoint_theme
or not os.path.exists(self.hass.config.path("themes"))
):
return
self.log.info("Setting up themes endpoint")
# Register themes
self.hass.http.register_static_path(f"{URL_BASE}/themes", self.hass.config.path("themes"))
self.status.active_frontend_endpoint_theme = True

View File

@ -1,29 +1,32 @@
"""Adds config flow for HACS."""
from __future__ import annotations
import asyncio
from contextlib import suppress
from typing import TYPE_CHECKING
from aiogithubapi import GitHubDeviceAPI, GitHubException
from aiogithubapi import (
GitHubDeviceAPI,
GitHubException,
GitHubLoginDeviceModel,
GitHubLoginOauthModel,
)
from aiogithubapi.common.const import OAUTH_USER_LOGIN
from awesomeversion import AwesomeVersion
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlow, OptionsFlow
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import callback
from homeassistant.data_entry_flow import UnknownFlow
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_call_later
from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase
from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION
from .enums import ConfigurationType
from .utils.configuration_schema import (
APPDAEMON,
COUNTRY,
DEBUG,
EXPERIMENTAL,
NETDAEMON,
RELEASE_LIMIT,
SIDEPANEL_ICON,
SIDEPANEL_TITLE,
)
@ -33,23 +36,22 @@ if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for HACS."""
hass: HomeAssistant
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
hass: HomeAssistant
activation_task: asyncio.Task | None = None
device: GitHubDeviceAPI | None = None
_registration: GitHubLoginDeviceModel | None = None
_activation: GitHubLoginOauthModel | None = None
_reauth: bool = False
def __init__(self) -> None:
"""Initialize."""
self._errors = {}
self.device = None
self.activation = None
self.log = LOGGER
self._progress_task = None
self._login_device = None
self._reauth = False
self._user_input = {}
async def async_step_user(self, user_input):
@ -69,48 +71,55 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_device(user_input)
## Initial form
# Initial form
return await self._show_config_form(user_input)
async def async_step_device(self, _user_input):
"""Handle device steps"""
"""Handle device steps."""
async def _wait_for_activation(_=None):
if self._login_device is None or self._login_device.expires_in is None:
async_call_later(self.hass, 1, _wait_for_activation)
return
async def _wait_for_activation() -> None:
try:
response = await self.device.activation(device_code=self._registration.device_code)
self._activation = response.data
finally:
response = await self.device.activation(device_code=self._login_device.device_code)
self.activation = response.data
self.hass.async_create_task(
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
)
async def _progress():
with suppress(UnknownFlow):
await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
if not self.activation:
if not self.device:
integration = await async_get_integration(self.hass, DOMAIN)
if not self.device:
self.device = GitHubDeviceAPI(
client_id=CLIENT_ID,
session=aiohttp_client.async_get_clientsession(self.hass),
**{"client_name": f"HACS/{integration.version}"},
)
async_call_later(self.hass, 1, _wait_for_activation)
self.device = GitHubDeviceAPI(
client_id=CLIENT_ID,
session=aiohttp_client.async_get_clientsession(self.hass),
**{"client_name": f"HACS/{integration.version}"},
)
try:
response = await self.device.register()
self._login_device = response.data
return self.async_show_progress(
step_id="device",
progress_action="wait_for_device",
description_placeholders={
"url": OAUTH_USER_LOGIN,
"code": self._login_device.user_code,
},
)
self._registration = response.data
except GitHubException as exception:
self.log.error(exception)
return self.async_abort(reason="github")
LOGGER.exception(exception)
return self.async_abort(reason="could_not_register")
return self.async_show_progress_done(next_step_id="device_done")
if self.activation_task is None:
self.activation_task = self.hass.async_create_task(_wait_for_activation())
if self.activation_task.done():
if (exception := self.activation_task.exception()) is not None:
LOGGER.exception(exception)
return self.async_show_progress_done(next_step_id="could_not_register")
return self.async_show_progress_done(next_step_id="device_done")
show_progress_kwargs = {
"step_id": "device",
"progress_action": "wait_for_device",
"description_placeholders": {
"url": OAUTH_USER_LOGIN,
"code": self._registration.user_code,
},
"progress_task": self.activation_task,
}
return self.async_show_progress(**show_progress_kwargs)
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
@ -133,9 +142,6 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"acc_untested", default=user_input.get("acc_untested", False)
): bool,
vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool,
vol.Optional(
"experimental", default=user_input.get("experimental", False)
): bool,
}
),
errors=self._errors,
@ -146,7 +152,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if self._reauth:
existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
self.hass.config_entries.async_update_entry(
existing_entry, data={**existing_entry.data, "token": self.activation.access_token}
existing_entry, data={**existing_entry.data, "token": self._activation.access_token}
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
@ -154,13 +160,17 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title="",
data={
"token": self.activation.access_token,
"token": self._activation.access_token,
},
options={
"experimental": self._user_input.get("experimental", False),
"experimental": True,
},
)
async def async_step_could_not_register(self, _user_input=None):
"""Handle issues that need transition await from progress step."""
return self.async_abort(reason="could_not_register")
async def async_step_reauth(self, _user_input=None):
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
@ -181,12 +191,13 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return HacsOptionsFlowHandler(config_entry)
class HacsOptionsFlowHandler(config_entries.OptionsFlow):
class HacsOptionsFlowHandler(OptionsFlow):
"""HACS config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
self.config_entry = config_entry
if AwesomeVersion(HAVERSION) < "2024.11.99":
self.config_entry = config_entry
async def async_step_init(self, _user_input=None):
"""Manage the options."""
@ -196,10 +207,7 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a flow initialized by the user."""
hacs: HacsBase = self.hass.data.get(DOMAIN)
if user_input is not None:
limit = int(user_input.get(RELEASE_LIMIT, 5))
if limit <= 0 or limit > 100:
return self.async_abort(reason="release_limit_value")
return self.async_create_entry(title="", data=user_input)
return self.async_create_entry(title="", data={**user_input, "experimental": True})
if hacs is None or hacs.configuration is None:
return self.async_abort(reason="not_setup")
@ -207,18 +215,11 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
if hacs.queue.has_pending_tasks:
return self.async_abort(reason="pending_tasks")
if hacs.configuration.config_type == ConfigurationType.YAML:
schema = {vol.Optional("not_in_use", default=""): str}
else:
schema = {
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
vol.Optional(RELEASE_LIMIT, default=hacs.configuration.release_limit): int,
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
vol.Optional(NETDAEMON, default=hacs.configuration.netdaemon): bool,
vol.Optional(DEBUG, default=hacs.configuration.debug): bool,
vol.Optional(EXPERIMENTAL, default=hacs.configuration.experimental): bool,
}
schema = {
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
}
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))

View File

@ -1,4 +1,5 @@
"""Constants for HACS"""
from typing import TypeVar
from aiogithubapi.common.const import ACCEPT_HEADERS
@ -6,7 +7,7 @@ from aiogithubapi.common.const import ACCEPT_HEADERS
NAME_SHORT = "HACS"
DOMAIN = "hacs"
CLIENT_ID = "395a8e669c5de9f7c6e8"
MINIMUM_HA_VERSION = "2023.6.0"
MINIMUM_HA_VERSION = "2024.4.1"
URL_BASE = "/hacsfiles"

View File

@ -1,12 +1,25 @@
"""HACS Data client."""
from __future__ import annotations
import asyncio
from typing import Any
from aiohttp import ClientSession, ClientTimeout
import voluptuous as vol
from .exceptions import HacsException, HacsNotModifiedException
from .utils.logger import LOGGER
from .utils.validate import (
VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REPO_DATA,
)
CRITICAL_REMOVED_VALIDATORS = {
"critical": VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
"removed": VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
}
class HacsDataClient:
@ -39,7 +52,7 @@ class HacsDataClient:
response.raise_for_status()
except HacsNotModifiedException:
raise
except asyncio.TimeoutError:
except TimeoutError:
raise HacsException("Timeout of 60s reached") from None
except Exception as exception:
raise HacsException(f"Error fetching data from HACS: {exception}") from exception
@ -48,9 +61,37 @@ class HacsDataClient:
return await response.json()
async def get_data(self, section: str | None) -> dict[str, dict[str, Any]]:
async def get_data(self, section: str | None, *, validate: bool) -> dict[str, dict[str, Any]]:
"""Get data."""
return await self._do_request(filename="data.json", section=section)
data = await self._do_request(filename="data.json", section=section)
if not validate:
return data
if section in VALIDATE_FETCHED_V2_REPO_DATA:
validated = {}
for key, repo_data in data.items():
try:
validated[key] = VALIDATE_FETCHED_V2_REPO_DATA[section](repo_data)
except vol.Invalid as exception:
LOGGER.info(
"Got invalid data for %s (%s)", repo_data.get("full_name", key), exception
)
continue
return validated
if not (validator := CRITICAL_REMOVED_VALIDATORS.get(section)):
raise ValueError(f"Do not know how to validate {section}")
validated = []
for repo_data in data:
try:
validated.append(validator(repo_data))
except vol.Invalid as exception:
LOGGER.info("Got invalid data for %s (%s)", section, exception)
continue
return validated
async def get_repositories(self, section: str) -> list[str]:
"""Get repositories."""

View File

@ -1,4 +1,5 @@
"""Diagnostics support for HACS."""
from __future__ import annotations
from typing import Any
@ -10,7 +11,6 @@ from homeassistant.core import HomeAssistant
from .base import HacsBase
from .const import DOMAIN
from .utils.configuration_schema import TOKEN
async def async_get_config_entry_diagnostics(
@ -48,8 +48,6 @@ async def async_get_config_entry_diagnostics(
"country",
"debug",
"dev",
"experimental",
"netdaemon",
"python_script",
"release_limit",
"theme",
@ -79,4 +77,4 @@ async def async_get_config_entry_diagnostics(
except GitHubException as exception:
data["rate_limit"] = str(exception)
return async_redact_data(data, (TOKEN,))
return async_redact_data(data, ("token",))

View File

@ -1,4 +1,5 @@
"""HACS Base entities."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
@ -7,8 +8,10 @@ from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity
from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
from .coordinator import HacsUpdateCoordinator
from .enums import HacsDispatchEvent, HacsGitHubRepo
if TYPE_CHECKING:
@ -39,6 +42,10 @@ class HacsBaseEntity(Entity):
"""Initialize."""
self.hacs = hacs
class HacsDispatcherEntity(HacsBaseEntity):
"""Base HACS entity listening to dispatcher signals."""
async def async_added_to_hass(self) -> None:
"""Register for status events."""
self.async_on_remove(
@ -64,7 +71,7 @@ class HacsBaseEntity(Entity):
self.async_write_ha_state()
class HacsSystemEntity(HacsBaseEntity):
class HacsSystemEntity(HacsDispatcherEntity):
"""Base system entity."""
_attr_icon = "hacs:hacs"
@ -76,7 +83,7 @@ class HacsSystemEntity(HacsBaseEntity):
return system_info(self.hacs)
class HacsRepositoryEntity(HacsBaseEntity):
class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity):
"""Base repository entity."""
def __init__(
@ -85,9 +92,11 @@ class HacsRepositoryEntity(HacsBaseEntity):
repository: HacsRepository,
) -> None:
"""Initialize."""
super().__init__(hacs=hacs)
BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category])
HacsBaseEntity.__init__(self, hacs=hacs)
self.repository = repository
self._attr_unique_id = str(repository.data.id)
self._repo_last_fetched = repository.data.last_fetched
@property
def available(self) -> bool:
@ -100,20 +109,35 @@ class HacsRepositoryEntity(HacsBaseEntity):
if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION:
return system_info(self.hacs)
def _manufacturer():
if authors := self.repository.data.authors:
return ", ".join(author.replace("@", "") for author in authors)
return self.repository.data.full_name.split("/")[0]
return {
"identifiers": {(DOMAIN, str(self.repository.data.id))},
"name": self.repository.display_name,
"model": self.repository.data.category,
"manufacturer": ", ".join(
author.replace("@", "") for author in self.repository.data.authors
),
"configuration_url": "homeassistant://hacs",
"manufacturer": _manufacturer(),
"configuration_url": f"homeassistant://hacs/repository/{self.repository.data.id}",
"entry_type": DeviceEntryType.SERVICE,
}
@callback
def _update_and_write_state(self, data: dict) -> None:
"""Update the entity and write state."""
if data.get("repository_id") == self.repository.data.id:
self._update()
self.async_write_ha_state()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if (
self._repo_last_fetched is not None
and self.repository.data.last_fetched is not None
and self._repo_last_fetched >= self.repository.data.last_fetched
):
return
self._repo_last_fetched = self.repository.data.last_fetched
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""

View File

@ -1,20 +1,7 @@
"""Helper constants."""
# pylint: disable=missing-class-docstring
import sys
if sys.version_info.minor >= 11:
# Needs Python 3.11
from enum import StrEnum # # pylint: disable=no-name-in-module
else:
try:
# https://github.com/home-assistant/core/blob/dev/homeassistant/backports/enum.py
# Considered internal to Home Assistant, can be removed whenever.
from homeassistant.backports.enum import StrEnum
except ImportError:
from enum import Enum
class StrEnum(str, Enum):
pass
from enum import StrEnum
class HacsGitHubRepo(StrEnum):
@ -29,7 +16,6 @@ class HacsCategory(StrEnum):
INTEGRATION = "integration"
LOVELACE = "lovelace"
PLUGIN = "plugin" # Kept for legacy purposes
NETDAEMON = "netdaemon"
PYTHON_SCRIPT = "python_script"
TEMPLATE = "template"
THEME = "theme"
@ -59,11 +45,6 @@ class RepositoryFile(StrEnum):
MAINIFEST_JSON = "manifest.json"
class ConfigurationType(StrEnum):
YAML = "yaml"
CONFIG_ENTRY = "config_entry"
class LovelaceMode(StrEnum):
"""Lovelace Modes."""

View File

@ -1,61 +1,53 @@
""""Starting setup task: Frontend"."""
"""Starting setup task: Frontend."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant, callback
from homeassistant.components.frontend import (
add_extra_js_url,
async_register_built_in_panel,
)
from .const import DOMAIN, URL_BASE
from .hacs_frontend import VERSION as FE_VERSION, locate_dir
from .hacs_frontend_experimental import (
VERSION as EXPERIMENTAL_FE_VERSION,
locate_dir as experimental_locate_dir,
)
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from .base import HacsBase
@callback
def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
"""Register the frontend."""
# Setup themes endpoint if needed
hacs.async_setup_frontend_endpoint_themes()
# Register frontend
if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")):
hacs.log.warning(
"<HacsFrontend> Frontend development mode enabled. Do not run in production!"
)
hass.http.register_static_path(
f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
)
elif hacs.configuration.experimental:
hacs.log.info("<HacsFrontend> Using experimental frontend")
hass.http.register_static_path(
f"{URL_BASE}/frontend", experimental_locate_dir(), cache_headers=False
await async_register_static_path(
hass, f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
)
hacs.frontend_version = "dev"
else:
#
hass.http.register_static_path(f"{URL_BASE}/frontend", locate_dir(), cache_headers=False)
await async_register_static_path(
hass, f"{URL_BASE}/frontend", locate_dir(), cache_headers=False
)
hacs.frontend_version = FE_VERSION
# Custom iconset
hass.http.register_static_path(
f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
)
if "frontend_extra_module_url" not in hass.data:
hass.data["frontend_extra_module_url"] = set()
hass.data["frontend_extra_module_url"].add(f"{URL_BASE}/iconset.js")
hacs.frontend_version = (
FE_VERSION if not hacs.configuration.experimental else EXPERIMENTAL_FE_VERSION
await async_register_static_path(
hass, f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
)
add_extra_js_url(hass, f"{URL_BASE}/iconset.js")
# Add to sidepanel if needed
if DOMAIN not in hass.data.get("frontend_panels", {}):
hass.components.frontend.async_register_built_in_panel(
async_register_built_in_panel(
hass,
component_name="custom",
sidebar_title=hacs.configuration.sidepanel_title,
sidebar_icon=hacs.configuration.sidepanel_icon,
@ -72,4 +64,4 @@ def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
)
# Setup plugin endpoint if needed
hacs.async_setup_frontend_endpoint_plugin()
await hacs.async_setup_frontend_endpoint_plugin()

File diff suppressed because one or more lines are too long

View File

@ -1,23 +0,0 @@
import{a as t,r as i,n as a}from"./main-ad130be7.js";import{L as n,s}from"./c.82eccc94.js";let r=t([a("ha-list-item")],(function(t,a){return{F:class extends a{constructor(...i){super(...i),t(this)}},d:[{kind:"get",static:!0,key:"styles",value:function(){return[s,i`
:host {
padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px);
}
:host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) {
height: 48px;
}
span.material-icons:first-of-type {
margin-inline-start: 0px !important;
margin-inline-end: var(
--mdc-list-item-graphic-margin,
16px
) !important;
direction: var(--direction);
}
span.material-icons:last-of-type {
margin-inline-start: auto !important;
margin-inline-end: 0px !important;
direction: var(--direction);
}
`]}}]}}),n);const e=t=>`https://brands.home-assistant.io/${t.useFallback?"_/":""}${t.domain}/${t.darkOptimized?"dark_":""}${t.type}.png`,o=t=>t.split("/")[4],p=t=>t.startsWith("https://brands.home-assistant.io/");export{r as H,e as b,o as e,p as i};

View File

@ -1,24 +0,0 @@
import{a as e,h as t,Y as i,e as n,i as o,$ as r,L as l,N as a,r as d,n as s}from"./main-ad130be7.js";import"./c.9b92f489.js";e([s("ha-button-menu")],(function(e,t){class s extends t{constructor(...t){super(...t),e(this)}}return{F:s,d:[{kind:"field",key:i,value:void 0},{kind:"field",decorators:[n()],key:"corner",value:()=>"TOP_START"},{kind:"field",decorators:[n()],key:"menuCorner",value:()=>"START"},{kind:"field",decorators:[n({type:Number})],key:"x",value:()=>null},{kind:"field",decorators:[n({type:Number})],key:"y",value:()=>null},{kind:"field",decorators:[n({type:Boolean})],key:"multi",value:()=>!1},{kind:"field",decorators:[n({type:Boolean})],key:"activatable",value:()=>!1},{kind:"field",decorators:[n({type:Boolean})],key:"disabled",value:()=>!1},{kind:"field",decorators:[n({type:Boolean})],key:"fixed",value:()=>!1},{kind:"field",decorators:[o("mwc-menu",!0)],key:"_menu",value:void 0},{kind:"get",key:"items",value:function(){var e;return null===(e=this._menu)||void 0===e?void 0:e.items}},{kind:"get",key:"selected",value:function(){var e;return null===(e=this._menu)||void 0===e?void 0:e.selected}},{kind:"method",key:"focus",value:function(){var e,t;null!==(e=this._menu)&&void 0!==e&&e.open?this._menu.focusItemAtIndex(0):null===(t=this._triggerButton)||void 0===t||t.focus()}},{kind:"method",key:"render",value:function(){return r`
<div @click=${this._handleClick}>
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
</div>
<mwc-menu
.corner=${this.corner}
.menuCorner=${this.menuCorner}
.fixed=${this.fixed}
.multi=${this.multi}
.activatable=${this.activatable}
.y=${this.y}
.x=${this.x}
>
<slot></slot>
</mwc-menu>
`}},{kind:"method",key:"firstUpdated",value:function(e){l(a(s.prototype),"firstUpdated",this).call(this,e),"rtl"===document.dir&&this.updateComplete.then((()=>{this.querySelectorAll("mwc-list-item").forEach((e=>{const t=document.createElement("style");t.innerHTML="span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}",e.shadowRoot.appendChild(t)}))}))}},{kind:"method",key:"_handleClick",value:function(){this.disabled||(this._menu.anchor=this,this._menu.show())}},{kind:"get",key:"_triggerButton",value:function(){return this.querySelector('ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]')}},{kind:"method",key:"_setTriggerAria",value:function(){this._triggerButton&&(this._triggerButton.ariaHasPopup="menu")}},{kind:"get",static:!0,key:"styles",value:function(){return d`
:host {
display: inline-block;
position: relative;
}
::slotted([disabled]) {
color: var(--disabled-text-color);
}
`}}]}}),t);

View File

@ -1,390 +0,0 @@
import{a as e,h as t,e as i,g as a,t as s,$ as o,j as r,R as n,w as l,r as h,n as c,m as d,L as p,N as u,o as v,b as f,aI as b,ai as m,c as k,E as g,aJ as y,aC as w,aK as x,aL as $,d as _,s as R}from"./main-ad130be7.js";import{f as z}from"./c.3243a8b0.js";import{c as j}from"./c.4a97632a.js";import"./c.f1291e50.js";import"./c.2d5ed670.js";import"./c.97b7c4b0.js";import{r as F}from"./c.4204ca09.js";import{i as P}from"./c.21c042d4.js";import{s as I}from"./c.2645c235.js";import"./c.a5f69ed4.js";import"./c.3f859915.js";import"./c.9b92f489.js";import"./c.82eccc94.js";import"./c.8e28b461.js";import"./c.4feb0cb8.js";import"./c.0ca5587f.js";import"./c.5d3ce9d6.js";import"./c.f6611997.js";import"./c.743a15a1.js";import"./c.4266acdb.js";e([c("ha-tab")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"active",value:()=>!1},{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"narrow",value:()=>!1},{kind:"field",decorators:[i()],key:"name",value:void 0},{kind:"field",decorators:[a("mwc-ripple")],key:"_ripple",value:void 0},{kind:"field",decorators:[s()],key:"_shouldRenderRipple",value:()=>!1},{kind:"method",key:"render",value:function(){return o`
<div
tabindex="0"
role="tab"
aria-selected=${this.active}
aria-label=${r(this.name)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
@keydown=${this._handleKeyDown}
>
${this.narrow?o`<slot name="icon"></slot>`:""}
<span class="name">${this.name}</span>
${this._shouldRenderRipple?o`<mwc-ripple></mwc-ripple>`:""}
</div>
`}},{kind:"field",key:"_rippleHandlers",value(){return new n((()=>(this._shouldRenderRipple=!0,this._ripple)))}},{kind:"method",key:"_handleKeyDown",value:function(e){13===e.keyCode&&e.target.click()}},{kind:"method",decorators:[l({passive:!0})],key:"handleRippleActivate",value:function(e){this._rippleHandlers.startPress(e)}},{kind:"method",key:"handleRippleDeactivate",value:function(){this._rippleHandlers.endPress()}},{kind:"method",key:"handleRippleMouseEnter",value:function(){this._rippleHandlers.startHover()}},{kind:"method",key:"handleRippleMouseLeave",value:function(){this._rippleHandlers.endHover()}},{kind:"method",key:"handleRippleFocus",value:function(){this._rippleHandlers.startFocus()}},{kind:"method",key:"handleRippleBlur",value:function(){this._rippleHandlers.endFocus()}},{kind:"get",static:!0,key:"styles",value:function(){return h`
div {
padding: 0 32px;
display: flex;
flex-direction: column;
text-align: center;
box-sizing: border-box;
align-items: center;
justify-content: center;
width: 100%;
height: var(--header-height);
cursor: pointer;
position: relative;
outline: none;
}
.name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
:host([active]) {
color: var(--primary-color);
}
:host(:not([narrow])[active]) div {
border-bottom: 2px solid var(--primary-color);
}
:host([narrow]) {
min-width: 0;
display: flex;
justify-content: center;
overflow: hidden;
}
:host([narrow]) div {
padding: 0 4px;
}
`}}]}}),t),e([c("hass-tabs-subpage")],(function(e,t){class a extends t{constructor(...t){super(...t),e(this)}}return{F:a,d:[{kind:"field",decorators:[i({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[i({type:Boolean})],key:"supervisor",value:()=>!1},{kind:"field",decorators:[i({attribute:!1})],key:"localizeFunc",value:void 0},{kind:"field",decorators:[i({type:String,attribute:"back-path"})],key:"backPath",value:void 0},{kind:"field",decorators:[i()],key:"backCallback",value:void 0},{kind:"field",decorators:[i({type:Boolean,attribute:"main-page"})],key:"mainPage",value:()=>!1},{kind:"field",decorators:[i({attribute:!1})],key:"route",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"tabs",value:void 0},{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"narrow",value:()=>!1},{kind:"field",decorators:[i({type:Boolean,reflect:!0,attribute:"is-wide"})],key:"isWide",value:()=>!1},{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"rtl",value:()=>!1},{kind:"field",decorators:[s()],key:"_activeTab",value:void 0},{kind:"field",decorators:[F(".content")],key:"_savedScrollPos",value:void 0},{kind:"field",key:"_getTabs",value(){return d(((e,t,i,a,s,r,n)=>{const l=e.filter((e=>(!e.component||e.core||P(this.hass,e.component))&&(!e.advancedOnly||i)));if(l.length<2){if(1===l.length){const e=l[0];return[e.translationKey?n(e.translationKey):e.name]}return[""]}return l.map((e=>o`
<a href=${e.path}>
<ha-tab
.hass=${this.hass}
.active=${e.path===(null==t?void 0:t.path)}
.narrow=${this.narrow}
.name=${e.translationKey?n(e.translationKey):e.name}
>
${e.iconPath?o`<ha-svg-icon
slot="icon"
.path=${e.iconPath}
></ha-svg-icon>`:""}
</ha-tab>
</a>
`))}))}},{kind:"method",key:"willUpdate",value:function(e){if(e.has("route")&&(this._activeTab=this.tabs.find((e=>`${this.route.prefix}${this.route.path}`.includes(e.path)))),e.has("hass")){const t=e.get("hass");t&&t.language===this.hass.language||(this.rtl=j(this.hass))}p(u(a.prototype),"willUpdate",this).call(this,e)}},{kind:"method",key:"render",value:function(){var e,t;const i=this._getTabs(this.tabs,this._activeTab,null===(e=this.hass.userData)||void 0===e?void 0:e.showAdvanced,this.hass.config.components,this.hass.language,this.narrow,this.localizeFunc||this.hass.localize),a=i.length>1;return o`
<div class="toolbar">
${this.mainPage||!this.backPath&&null!==(t=history.state)&&void 0!==t&&t.root?o`
<ha-menu-button
.hassio=${this.supervisor}
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`:this.backPath?o`
<a href=${this.backPath}>
<ha-icon-button-arrow-prev
.hass=${this.hass}
></ha-icon-button-arrow-prev>
</a>
`:o`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
${this.narrow||!a?o`<div class="main-title">
<slot name="header">${a?"":i[0]}</slot>
</div>`:""}
${a?o`
<div id="tabbar" class=${v({"bottom-bar":this.narrow})}>
${i}
</div>
`:""}
<div id="toolbar-icon">
<slot name="toolbar-icon"></slot>
</div>
</div>
<div
class="content ${v({tabs:a})}"
@scroll=${this._saveScrollPos}
>
<slot></slot>
</div>
<div id="fab" class=${v({tabs:a})}>
<slot name="fab"></slot>
</div>
`}},{kind:"method",decorators:[l({passive:!0})],key:"_saveScrollPos",value:function(e){this._savedScrollPos=e.target.scrollTop}},{kind:"method",key:"_backTapped",value:function(){this.backCallback?this.backCallback():history.back()}},{kind:"get",static:!0,key:"styles",value:function(){return h`
:host {
display: block;
height: 100%;
background-color: var(--primary-background-color);
}
:host([narrow]) {
width: 100%;
position: fixed;
}
ha-menu-button {
margin-right: 24px;
}
.toolbar {
display: flex;
align-items: center;
font-size: 20px;
height: var(--header-height);
background-color: var(--sidebar-background-color);
font-weight: 400;
border-bottom: 1px solid var(--divider-color);
padding: 0 16px;
box-sizing: border-box;
}
.toolbar a {
color: var(--sidebar-text-color);
text-decoration: none;
}
.bottom-bar a {
width: 25%;
}
#tabbar {
display: flex;
font-size: 14px;
overflow: hidden;
}
#tabbar > a {
overflow: hidden;
max-width: 45%;
}
#tabbar.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
padding: 0 16px;
box-sizing: border-box;
background-color: var(--sidebar-background-color);
border-top: 1px solid var(--divider-color);
justify-content: space-around;
z-index: 2;
font-size: 12px;
width: 100%;
padding-bottom: env(safe-area-inset-bottom);
}
#tabbar:not(.bottom-bar) {
flex: 1;
justify-content: center;
}
:host(:not([narrow])) #toolbar-icon {
min-width: 40px;
}
ha-menu-button,
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
display: flex;
flex-shrink: 0;
pointer-events: auto;
color: var(--sidebar-icon-color);
}
.main-title {
flex: 1;
max-height: var(--header-height);
line-height: 20px;
color: var(--sidebar-text-color);
margin: var(--main-title-margin, 0 0 0 24px);
}
.content {
position: relative;
width: calc(
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
height: calc(100% - 1px - var(--header-height));
height: calc(
100% - 1px - var(--header-height) - env(safe-area-inset-bottom)
);
overflow: auto;
-webkit-overflow-scrolling: touch;
}
:host([narrow]) .content.tabs {
height: calc(100% - 2 * var(--header-height));
height: calc(
100% - 2 * var(--header-height) - env(safe-area-inset-bottom)
);
}
#fab {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
:host([narrow]) #fab.tabs {
bottom: calc(84px + env(safe-area-inset-bottom));
}
#fab[is-wide] {
bottom: 24px;
right: 24px;
}
:host([rtl]) #fab {
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
:host([rtl][is-wide]) #fab {
bottom: 24px;
left: 24px;
right: auto;
}
`}}]}}),t);let E=e([c("hacs-store-panel")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[i({attribute:!1})],key:"filters",value:()=>({})},{kind:"field",decorators:[i({attribute:!1})],key:"hacs",value:void 0},{kind:"field",decorators:[i()],key:"_searchInput",value:()=>""},{kind:"field",decorators:[i({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"narrow",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"isWide",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"route",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"sections",value:void 0},{kind:"field",decorators:[i()],key:"section",value:void 0},{kind:"field",key:"_repositoriesInActiveSection",value(){return d(((e,t)=>[(null==e?void 0:e.filter((e=>{var i,a,s;return(null===(i=this.hacs.sections)||void 0===i||null===(a=i.find((e=>e.id===t)))||void 0===a||null===(s=a.categories)||void 0===s?void 0:s.includes(e.category))&&e.installed})))||[],(null==e?void 0:e.filter((e=>{var i,a,s;return(null===(i=this.hacs.sections)||void 0===i||null===(a=i.find((e=>e.id===t)))||void 0===a||null===(s=a.categories)||void 0===s?void 0:s.includes(e.category))&&e.new&&!e.installed})))||[]]))}},{kind:"get",key:"allRepositories",value:function(){const[e,t]=this._repositoriesInActiveSection(this.hacs.repositories,this.section);return t.concat(e)}},{kind:"field",key:"_filterRepositories",value:()=>d(z)},{kind:"get",key:"visibleRepositories",value:function(){const e=this.allRepositories.filter((e=>{var t,i;return null===(t=this.filters[this.section])||void 0===t||null===(i=t.find((t=>t.id===e.category)))||void 0===i?void 0:i.checked}));return this._filterRepositories(e,this._searchInput)}},{kind:"method",key:"firstUpdated",value:async function(){this.addEventListener("filter-change",(e=>this._updateFilters(e)))}},{kind:"method",key:"_updateFilters",value:function(e){var t;const i=null===(t=this.filters[this.section])||void 0===t?void 0:t.find((t=>t.id===e.detail.id));this.filters[this.section].find((e=>e.id===i.id)).checked=!i.checked,this.requestUpdate()}},{kind:"method",key:"render",value:function(){var e;if(!this.hacs)return o``;const t=this._repositoriesInActiveSection(this.hacs.repositories,this.section)[1];if(!this.filters[this.section]&&this.hacs.info.categories){var i;const e=null===(i=f(this.hacs.language,this.route))||void 0===i?void 0:i.categories;this.filters[this.section]=[],null==e||e.filter((e=>{var t;return null===(t=this.hacs.info)||void 0===t?void 0:t.categories.includes(e)})).forEach((e=>{this.filters[this.section].push({id:e,value:e,checked:!0})}))}return o`<hass-tabs-subpage
back-path="/hacs/entry"
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${this.hacs.sections}
hasFab
>
<ha-icon-overflow-menu
slot="toolbar-icon"
narrow
.hass=${this.hass}
.items=${[{path:b,label:this.hacs.localize("menu.documentation"),action:()=>m.open("https://hacs.xyz/","_blank","noreferrer=true")},{path:k,label:"GitHub",action:()=>m.open("https://github.com/hacs","_blank","noreferrer=true")},{path:g,label:this.hacs.localize("menu.open_issue"),action:()=>m.open("https://hacs.xyz/docs/issues","_blank","noreferrer=true")},{path:y,label:this.hacs.localize("menu.custom_repositories"),disabled:this.hacs.info.disabled_reason,action:()=>this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"custom-repositories",repositories:this.hacs.repositories},bubbles:!0,composed:!0}))},{path:w,label:this.hacs.localize("menu.about"),action:()=>I(this,this.hacs)}]}
>
</ha-icon-overflow-menu>
${this.narrow?o`
<search-input
.hass=${this.hass}
class="header"
slot="header"
.label=${this.hacs.localize("search.downloaded")}
.filter=${this._searchInput||""}
@value-changed=${this._inputValueChanged}
></search-input>
`:o`<div class="search">
<search-input
.hass=${this.hass}
.label=${0===t.length?this.hacs.localize("search.downloaded"):this.hacs.localize("search.downloaded_new")}
.filter=${this._searchInput||""}
@value-changed=${this._inputValueChanged}
></search-input>
</div>`}
<div class="content ${this.narrow?"narrow-content":""}">
${(null===(e=this.filters[this.section])||void 0===e?void 0:e.length)>1?o`<div class="filters">
<hacs-filter
.hacs=${this.hacs}
.filters="${this.filters[this.section]}"
></hacs-filter>
</div>`:""}
${null!=t&&t.length?o`<ha-alert .rtl=${j(this.hass)}>
${this.hacs.localize("store.new_repositories_note")}
<mwc-button
class="max-content"
slot="action"
.label=${this.hacs.localize("menu.dismiss")}
@click=${this._clearAllNewRepositories}
>
</mwc-button>
</ha-alert> `:""}
<div class="container ${this.narrow?"narrow":""}">
${void 0===this.hacs.repositories?"":0===this.allRepositories.length?this._renderEmpty():0===this.visibleRepositories.length?this._renderNoResultsFound():this._renderRepositories()}
</div>
</div>
<ha-fab
slot="fab"
.label=${this.hacs.localize("store.explore")}
.extended=${!this.narrow}
@click=${this._addRepository}
>
<ha-svg-icon slot="icon" .path=${x}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage>`}},{kind:"method",key:"_renderRepositories",value:function(){return this.visibleRepositories.map((e=>o`<hacs-repository-card
.hass=${this.hass}
.hacs=${this.hacs}
.repository=${e}
.narrow=${this.narrow}
?narrow=${this.narrow}
></hacs-repository-card>`))}},{kind:"method",key:"_clearAllNewRepositories",value:async function(){var e;await $(this.hass,{categories:(null===(e=f(this.hacs.language,this.route))||void 0===e?void 0:e.categories)||[]})}},{kind:"method",key:"_renderNoResultsFound",value:function(){return o`<ha-alert
.rtl=${j(this.hass)}
alert-type="warning"
.title="${this.hacs.localize("store.no_repositories")} 😕"
>
${this.hacs.localize("store.no_repositories_found_desc1",{searchInput:this._searchInput})}
<br />
${this.hacs.localize("store.no_repositories_found_desc2")}
</ha-alert>`}},{kind:"method",key:"_renderEmpty",value:function(){return o`<ha-alert
.title="${this.hacs.localize("store.no_repositories")} 😕"
.rtl=${j(this.hass)}
>
${this.hacs.localize("store.no_repositories_desc1")}
<br />
${this.hacs.localize("store.no_repositories_desc2")}
</ha-alert>`}},{kind:"method",key:"_inputValueChanged",value:function(e){this._searchInput=e.detail.value,window.localStorage.setItem("hacs-search",this._searchInput)}},{kind:"method",key:"_addRepository",value:function(){this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"add-repository",repositories:this.hacs.repositories,section:this.section},bubbles:!0,composed:!0}))}},{kind:"get",static:!0,key:"styles",value:function(){return[_,R,h`
.filter {
border-bottom: 1px solid var(--divider-color);
}
.content {
height: calc(100vh - 128px);
overflow: auto;
}
.narrow-content {
height: calc(100vh - 128px);
}
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(480px, 1fr));
justify-items: center;
grid-gap: 8px 8px;
padding: 8px 16px 16px;
margin-bottom: 64px;
}
ha-svg-icon {
color: var(--hcv-text-color-on-background);
}
hacs-repository-card {
max-width: 500px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
hacs-repository-card[narrow] {
width: 100%;
}
hacs-repository-card[narrow]:last-of-type {
margin-bottom: 64px;
}
ha-alert {
color: var(--hcv-text-color-primary);
display: block;
margin-top: -4px;
}
.narrow {
width: 100%;
display: block;
padding: 0px;
margin: 0;
}
search-input {
display: block;
}
search-input.header {
padding: 0;
}
.bottom-bar {
position: fixed !important;
}
.max-content {
width: max-content;
}
`]}}]}}),t);export{E as HacsStorePanel};

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
import{a as e,e as t,i,L as a,N as d,$ as r,r as n,n as o}from"./main-ad130be7.js";import{H as s}from"./c.0a1cf8d0.js";e([o("ha-clickable-list-item")],(function(e,o){class s extends o{constructor(...t){super(...t),e(this)}}return{F:s,d:[{kind:"field",decorators:[t()],key:"href",value:void 0},{kind:"field",decorators:[t({type:Boolean})],key:"disableHref",value:()=>!1},{kind:"field",decorators:[t({type:Boolean,reflect:!0})],key:"openNewTab",value:()=>!1},{kind:"field",decorators:[i("a")],key:"_anchor",value:void 0},{kind:"method",key:"render",value:function(){const e=a(d(s.prototype),"render",this).call(this),t=this.href||"";return r`${this.disableHref?r`<a aria-role="option">${e}</a>`:r`<a
aria-role="option"
target=${this.openNewTab?"_blank":""}
href=${t}
>${e}</a
>`}`}},{kind:"method",key:"firstUpdated",value:function(){a(d(s.prototype),"firstUpdated",this).call(this),this.addEventListener("keydown",(e=>{"Enter"!==e.key&&" "!==e.key||this._anchor.click()}))}},{kind:"get",static:!0,key:"styles",value:function(){return[a(d(s),"styles",this),n`
a {
width: 100%;
height: 100%;
display: flex;
align-items: center;
padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px);
overflow: hidden;
}
`]}}]}}),s);

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
const n=(n,o)=>n&&n.config.components.includes(o);export{n as i};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{al as e,am as a,aj as s,an as r,ao as u}from"./main-ad130be7.js";async function i(i,o,n){const t=new e("updateLovelaceResources"),l=await a(i),d=`/hacsfiles/${o.full_name.split("/")[1]}`,c=s({repository:o,version:n}),p=l.find((e=>e.url.includes(d)));t.debug({namespace:d,url:c,exsisting:p}),p&&p.url!==c?(t.debug(`Updating exsusting resource for ${d}`),await r(i,{url:c,resource_id:p.id,res_type:p.type})):l.map((e=>e.url)).includes(c)||(t.debug(`Adding ${c} to Lovelace resources`),await u(i,{url:c,res_type:"module"}))}export{i as u};

View File

@ -1 +0,0 @@
import{m as o}from"./c.f6611997.js";import{a as t}from"./c.4266acdb.js";const n=async(n,s)=>t(n,{title:"Home Assistant Community Store",confirmText:s.localize("common.close"),text:o.html(`\n **${s.localize("dialog_about.integration_version")}:** | ${s.info.version}\n --|--\n **${s.localize("dialog_about.frontend_version")}:** | 20220906112053\n **${s.localize("common.repositories")}:** | ${s.repositories.length}\n **${s.localize("dialog_about.downloaded_repositories")}:** | ${s.repositories.filter((o=>o.installed)).length}\n\n **${s.localize("dialog_about.useful_links")}:**\n\n - [General documentation](https://hacs.xyz/)\n - [Configuration](https://hacs.xyz/docs/configuration/start)\n - [FAQ](https://hacs.xyz/docs/faq/what)\n - [GitHub](https://github.com/hacs)\n - [Discord](https://discord.gg/apgchf8)\n - [Become a GitHub sponsor? ❤️](https://github.com/sponsors/ludeeus)\n - [BuyMe~~Coffee~~Beer? 🍺🙈](https://buymeacoffee.com/ludeeus)\n\n ***\n\n _Everything you find in HACS is **not** tested by Home Assistant, that includes HACS itself.\n The HACS and Home Assistant teams do not support **anything** you find here._`)});export{n as s};

View File

@ -1,61 +0,0 @@
import{a as r,h as a,e as o,r as e,$ as d,n as t}from"./main-ad130be7.js";r([t("ha-card")],(function(r,a){return{F:class extends a{constructor(...a){super(...a),r(this)}},d:[{kind:"field",decorators:[o()],key:"header",value:void 0},{kind:"field",decorators:[o({type:Boolean,reflect:!0})],key:"outlined",value:()=>!1},{kind:"get",static:!0,key:"styles",value:function(){return e`
:host {
background: var(
--ha-card-background,
var(--card-background-color, white)
);
border-radius: var(--ha-card-border-radius, 4px);
box-shadow: var(
--ha-card-box-shadow,
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
);
color: var(--primary-text-color);
display: block;
transition: all 0.3s ease-out;
position: relative;
}
:host([outlined]) {
box-shadow: none;
border-width: var(--ha-card-border-width, 1px);
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
}
.card-header,
:host ::slotted(.card-header) {
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: 48px;
padding: 12px 16px 16px;
display: block;
margin-block-start: 0px;
margin-block-end: 0px;
font-weight: normal;
}
:host ::slotted(.card-content:not(:first-child)),
slot:not(:first-child)::slotted(.card-content) {
padding-top: 0px;
margin-top: -8px;
}
:host ::slotted(.card-content) {
padding: 16px;
}
:host ::slotted(.card-actions) {
border-top: 1px solid var(--divider-color, #e8e8e8);
padding: 5px 16px;
}
`}},{kind:"method",key:"render",value:function(){return d`
${this.header?d`<h1 class="card-header">${this.header}</h1>`:d``}
<slot></slot>
`}}]}}),a);

View File

@ -1,121 +0,0 @@
import{a as e,h as t,e as n,t as i,i as o,$ as a,av as d,o as s,L as r,N as l,A as h,ae as c,r as p,n as u}from"./main-ad130be7.js";e([u("ha-expansion-panel")],(function(e,t){class u extends t{constructor(...t){super(...t),e(this)}}return{F:u,d:[{kind:"field",decorators:[n({type:Boolean,reflect:!0})],key:"expanded",value:()=>!1},{kind:"field",decorators:[n({type:Boolean,reflect:!0})],key:"outlined",value:()=>!1},{kind:"field",decorators:[n({type:Boolean,reflect:!0})],key:"leftChevron",value:()=>!1},{kind:"field",decorators:[n()],key:"header",value:void 0},{kind:"field",decorators:[n()],key:"secondary",value:void 0},{kind:"field",decorators:[i()],key:"_showContent",value(){return this.expanded}},{kind:"field",decorators:[o(".container")],key:"_container",value:void 0},{kind:"method",key:"render",value:function(){return a`
<div class="top">
<div
id="summary"
@click=${this._toggleContainer}
@keydown=${this._toggleContainer}
@focus=${this._focusChanged}
@blur=${this._focusChanged}
role="button"
tabindex="0"
aria-expanded=${this.expanded}
aria-controls="sect1"
>
${this.leftChevron?a`
<ha-svg-icon
.path=${d}
class="summary-icon ${s({expanded:this.expanded})}"
></ha-svg-icon>
`:""}
<slot name="header">
<div class="header">
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</div>
</slot>
${this.leftChevron?"":a`
<ha-svg-icon
.path=${d}
class="summary-icon ${s({expanded:this.expanded})}"
></ha-svg-icon>
`}
</div>
<slot name="icons"></slot>
</div>
<div
class="container ${s({expanded:this.expanded})}"
@transitionend=${this._handleTransitionEnd}
role="region"
aria-labelledby="summary"
aria-hidden=${!this.expanded}
tabindex="-1"
>
${this._showContent?a`<slot></slot>`:""}
</div>
`}},{kind:"method",key:"willUpdate",value:function(e){r(l(u.prototype),"willUpdate",this).call(this,e),e.has("expanded")&&this.expanded&&(this._showContent=this.expanded,setTimeout((()=>{this.expanded&&(this._container.style.overflow="initial")}),300))}},{kind:"method",key:"_handleTransitionEnd",value:function(){this._container.style.removeProperty("height"),this._container.style.overflow=this.expanded?"initial":"hidden",this._showContent=this.expanded}},{kind:"method",key:"_toggleContainer",value:async function(e){if(e.defaultPrevented)return;if("keydown"===e.type&&"Enter"!==e.key&&" "!==e.key)return;e.preventDefault();const t=!this.expanded;h(this,"expanded-will-change",{expanded:t}),this._container.style.overflow="hidden",t&&(this._showContent=!0,await c());const n=this._container.scrollHeight;this._container.style.height=`${n}px`,t||setTimeout((()=>{this._container.style.height="0px"}),0),this.expanded=t,h(this,"expanded-changed",{expanded:this.expanded})}},{kind:"method",key:"_focusChanged",value:function(e){this.shadowRoot.querySelector(".top").classList.toggle("focused","focus"===e.type)}},{kind:"get",static:!0,key:"styles",value:function(){return p`
:host {
display: block;
}
.top {
display: flex;
align-items: center;
}
.top.focused {
background: var(--input-fill-color);
}
:host([outlined]) {
box-shadow: none;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border-radius: var(--ha-card-border-radius, 4px);
}
.summary-icon {
margin-left: 8px;
}
:host([leftchevron]) .summary-icon {
margin-left: 0;
margin-right: 8px;
}
#summary {
flex: 1;
display: flex;
padding: var(--expansion-panel-summary-padding, 0 8px);
min-height: 48px;
align-items: center;
cursor: pointer;
overflow: hidden;
font-weight: 500;
outline: none;
}
.summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
direction: var(--direction);
}
.summary-icon.expanded {
transform: rotate(180deg);
}
.header,
::slotted([slot="header"]) {
flex: 1;
}
.container {
padding: var(--expansion-panel-content-padding, 0 8px);
overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
height: 0px;
}
.container.expanded {
height: auto;
}
.secondary {
display: block;
color: var(--secondary-text-color);
font-size: 12px;
}
`}}]}}),t);

View File

@ -1,50 +0,0 @@
import{a as e,h as i,e as t,i as a,$ as n,O as l,z as o,A as s,r as c,n as r,m as d}from"./main-ad130be7.js";import"./c.3f859915.js";e([r("search-input")],(function(e,i){return{F:class extends i{constructor(...i){super(...i),e(this)}},d:[{kind:"field",decorators:[t({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[t()],key:"filter",value:void 0},{kind:"field",decorators:[t({type:Boolean})],key:"suffix",value:()=>!1},{kind:"field",decorators:[t({type:Boolean})],key:"autofocus",value:()=>!1},{kind:"field",decorators:[t({type:String})],key:"label",value:void 0},{kind:"method",key:"focus",value:function(){var e;null===(e=this._input)||void 0===e||e.focus()}},{kind:"field",decorators:[a("ha-textfield",!0)],key:"_input",value:void 0},{kind:"method",key:"render",value:function(){return n`
<ha-textfield
.autofocus=${this.autofocus}
.label=${this.label||"Search"}
.value=${this.filter||""}
icon
.iconTrailing=${this.filter||this.suffix}
@input=${this._filterInputChanged}
>
<slot name="prefix" slot="leadingIcon">
<ha-svg-icon
tabindex="-1"
class="prefix"
.path=${l}
></ha-svg-icon>
</slot>
<div class="trailing" slot="trailingIcon">
${this.filter&&n`
<ha-icon-button
@click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")}
.path=${o}
class="clear-button"
></ha-icon-button>
`}
<slot name="suffix"></slot>
</div>
</ha-textfield>
`}},{kind:"method",key:"_filterChanged",value:async function(e){s(this,"value-changed",{value:String(e)})}},{kind:"method",key:"_filterInputChanged",value:async function(e){this._filterChanged(e.target.value)}},{kind:"method",key:"_clearSearch",value:async function(){this._filterChanged("")}},{kind:"get",static:!0,key:"styles",value:function(){return c`
:host {
display: inline-flex;
}
ha-svg-icon,
ha-icon-button {
color: var(--primary-text-color);
}
ha-svg-icon {
outline: none;
}
.clear-button {
--mdc-icon-size: 20px;
}
ha-textfield {
display: inherit;
}
.trailing {
display: flex;
align-items: center;
}
`}}]}}),i);const u=d(((e,i)=>e.filter((e=>h(e.name).includes(h(i))||h(e.description).includes(h(i))||h(e.category).includes(h(i))||h(e.full_name).includes(h(i))||h(e.authors).includes(h(i))||h(e.domain).includes(h(i)))))),h=d((e=>String(e||"").toLocaleLowerCase().replace(/-|_| /g,"")));export{u as f};

View File

@ -1,94 +0,0 @@
import{a6 as e,a7 as t,a as o,h as i,e as n,$ as a,r,n as l}from"./main-ad130be7.js";e({_template:t`
<style>
:host {
overflow: hidden; /* needed for text-overflow: ellipsis to work on ff */
@apply --layout-vertical;
@apply --layout-center-justified;
@apply --layout-flex;
}
:host([two-line]) {
min-height: var(--paper-item-body-two-line-min-height, 72px);
}
:host([three-line]) {
min-height: var(--paper-item-body-three-line-min-height, 88px);
}
:host > ::slotted(*) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:host > ::slotted([secondary]) {
@apply --paper-font-body1;
color: var(--paper-item-body-secondary-color, var(--secondary-text-color));
@apply --paper-item-body-secondary;
}
</style>
<slot></slot>
`,is:"paper-item-body"}),o([l("ha-settings-row")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[n({type:Boolean,reflect:!0})],key:"narrow",value:void 0},{kind:"field",decorators:[n({type:Boolean,attribute:"three-line"})],key:"threeLine",value:()=>!1},{kind:"method",key:"render",value:function(){return a`
<div class="prefix-wrap">
<slot name="prefix"></slot>
<paper-item-body
?two-line=${!this.threeLine}
?three-line=${this.threeLine}
>
<slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>
</paper-item-body>
</div>
<div class="content"><slot></slot></div>
`}},{kind:"get",static:!0,key:"styles",value:function(){return r`
:host {
display: flex;
padding: 0 16px;
align-content: normal;
align-self: auto;
align-items: center;
}
paper-item-body {
padding: 8px 16px 8px 0;
}
paper-item-body[two-line] {
min-height: calc(
var(--paper-item-body-two-line-min-height, 72px) - 16px
);
flex: 1;
}
.content {
display: contents;
}
:host(:not([narrow])) .content {
display: var(--settings-row-content-display, flex);
justify-content: flex-end;
flex: 1;
padding: 16px 0;
}
.content ::slotted(*) {
width: var(--settings-row-content-width);
}
:host([narrow]) {
align-items: normal;
flex-direction: column;
border-top: 1px solid var(--divider-color);
padding-bottom: 8px;
}
::slotted(ha-switch) {
padding: 16px 0;
}
div[secondary] {
white-space: normal;
}
.prefix-wrap {
display: var(--settings-row-prefix-display);
}
:host([narrow]) .prefix-wrap {
display: flex;
align-items: center;
}
`}}]}}),i);

File diff suppressed because one or more lines are too long

View File

@ -1,190 +0,0 @@
import{a as e,h as t,e as o,$ as r,aM as i,r as a,n as s,o as n,aL as d,d as c}from"./main-ad130be7.js";import"./c.2d5ed670.js";import"./c.9b92f489.js";import"./c.82eccc94.js";import"./c.4feb0cb8.js";import"./c.0ca5587f.js";import"./c.5d3ce9d6.js";e([s("ha-icon-overflow-menu")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[o({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[o({type:Array})],key:"items",value:()=>[]},{kind:"field",decorators:[o({type:Boolean})],key:"narrow",value:()=>!1},{kind:"method",key:"render",value:function(){return r`
${this.narrow?r` <!-- Collapsed representation for small screens -->
<ha-button-menu
@click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed}
class="ha-icon-overflow-menu-overflow"
corner="BOTTOM_START"
absolute
>
<ha-icon-button
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${i}
slot="trigger"
></ha-icon-button>
${this.items.map((e=>r`
<mwc-list-item
graphic="icon"
.disabled=${e.disabled}
@click=${e.action}
>
<div slot="graphic">
<ha-svg-icon .path=${e.path}></ha-svg-icon>
</div>
${e.label}
</mwc-list-item>
`))}
</ha-button-menu>`:r`
<!-- Icon representation for big screens -->
${this.items.map((e=>e.narrowOnly?"":r`<div>
${e.tooltip?r`<paper-tooltip animation-delay="0" position="left">
${e.tooltip}
</paper-tooltip>`:""}
<ha-icon-button
@click=${e.action}
.label=${e.label}
.path=${e.path}
.disabled=${e.disabled}
></ha-icon-button>
</div> `))}
`}
`}},{kind:"method",key:"_handleIconOverflowMenuOpened",value:function(){const e=this.closest(".mdc-data-table__row");e&&(e.style.zIndex="1")}},{kind:"method",key:"_handleIconOverflowMenuClosed",value:function(){const e=this.closest(".mdc-data-table__row");e&&(e.style.zIndex="")}},{kind:"get",static:!0,key:"styles",value:function(){return a`
:host {
display: flex;
justify-content: flex-end;
}
`}}]}}),t);const l=e=>t=>({kind:"method",placement:"prototype",key:t.key,descriptor:{set(e){this[`__${String(t.key)}`]=e},get(){return this[`__${String(t.key)}`]},enumerable:!0,configurable:!0},finisher(o){const r=o.prototype.connectedCallback;o.prototype.connectedCallback=function(){if(r.call(this),this[t.key]){const o=this.renderRoot.querySelector(e);if(!o)return;o.scrollTop=this[t.key]}}}});e([s("hacs-repository-card")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[o({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[o({attribute:!1})],key:"hacs",value:void 0},{kind:"field",decorators:[o({attribute:!1})],key:"repository",value:void 0},{kind:"field",decorators:[o({type:Boolean})],key:"narrow",value:void 0},{kind:"get",key:"_borderClass",value:function(){const e={};return this.hacs.addedToLovelace(this.hacs,this.repository)&&"pending-restart"!==this.repository.status?this.repository.pending_upgrade?e["status-update"]=!0:this.repository.new&&!this.repository.installed&&(e["status-new"]=!0):e["status-issue"]=!0,0!==Object.keys(e).length&&(e["status-border"]=!0),e}},{kind:"get",key:"_headerClass",value:function(){const e={};return this.hacs.addedToLovelace(this.hacs,this.repository)&&"pending-restart"!==this.repository.status?this.repository.pending_upgrade?e["update-header"]=!0:this.repository.new&&!this.repository.installed?e["new-header"]=!0:e["default-header"]=!0:e["issue-header"]=!0,e}},{kind:"get",key:"_headerTitle",value:function(){return this.hacs.addedToLovelace(this.hacs,this.repository)?"pending-restart"===this.repository.status?this.hacs.localize("repository_card.pending_restart"):this.repository.pending_upgrade?this.hacs.localize("repository_card.pending_update"):this.repository.new&&!this.repository.installed?this.hacs.localize("repository_card.new_repository"):"":this.hacs.localize("repository_card.not_loaded")}},{kind:"method",key:"render",value:function(){return r`
<a href="/hacs/repository/${this.repository.id}">
<ha-card class=${n(this._borderClass)} ?narrow=${this.narrow} outlined>
<div class="card-content">
<div class="group-header">
<div class="status-header ${n(this._headerClass)}">${this._headerTitle}</div>
<div class="title pointer">
<h1>${this.repository.name}</h1>
${"integration"!==this.repository.category?r` <ha-chip>
${this.hacs.localize(`common.${this.repository.category}`)}
</ha-chip>`:""}
</div>
</div>
<div class="description">${this.repository.description}</div>
</div>
<div class="card-actions">
${this.repository.new&&!this.repository.installed?r`<div>
<mwc-button class="status-new" @click=${this._setNotNew}>
${this.hacs.localize("repository_card.dismiss")}
</mwc-button>
</div>`:this.repository.pending_upgrade&&this.hacs.addedToLovelace(this.hacs,this.repository)?r`<div>
<mwc-button class="update-header" @click=${this._updateRepository} raised>
${this.hacs.localize("common.update")}
</mwc-button>
</div> `:""}
</div>
</ha-card>
</a>
`}},{kind:"method",key:"_updateRepository",value:function(e){e.preventDefault(),this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"update",repository:this.repository.id},bubbles:!0,composed:!0}))}},{kind:"method",key:"_setNotNew",value:async function(e){e.preventDefault(),await d(this.hass,{repository:String(this.repository.id)})}},{kind:"get",static:!0,key:"styles",value:function(){return[c,a`
ha-card {
display: flex;
flex-direction: column;
height: 195px;
width: 480px;
}
.title {
display: flex;
justify-content: space-between;
}
.card-content {
padding: 0 0 3px 0;
height: 100%;
}
.card-actions {
border-top: none;
bottom: 0;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
padding: 5px;
}
.group-header {
height: auto;
align-content: center;
}
.group-header h1 {
margin: 0;
padding: 8px 16px;
font-size: 22px;
}
h1 {
margin-top: 0;
min-height: 24px;
}
a {
all: unset;
cursor: pointer;
}
.description {
opacity: var(--dark-primary-opacity);
font-size: 14px;
padding: 8px 16px;
max-height: 52px;
overflow: hidden;
}
.status-new {
border-color: var(--hcv-color-new);
--mdc-theme-primary: var(--hcv-color-new);
}
.status-update {
border-color: var(--hcv-color-update);
}
.status-issue {
border-color: var(--hcv-color-error);
}
.new-header {
background-color: var(--hcv-color-new);
color: var(--hcv-text-color-on-background);
}
.issue-header {
background-color: var(--hcv-color-error);
color: var(--hcv-text-color-on-background);
}
.update-header {
background-color: var(--hcv-color-update);
color: var(--hcv-text-color-on-background);
}
.default-header {
padding: 2px 0 !important;
}
mwc-button.update-header {
--mdc-theme-primary: var(--hcv-color-update);
--mdc-theme-on-primary: var(--hcv-text-color-on-background);
}
.status-border {
border-style: solid;
border-width: min(var(--ha-card-border-width, 1px), 10px);
}
.status-header {
top: 0;
padding: 6px 1px;
margin: -1px;
width: 100%;
font-weight: 500;
text-align: center;
left: 0;
border-top-left-radius: var(--ha-card-border-radius, 4px);
border-top-right-radius: var(--ha-card-border-radius, 4px);
}
ha-card[narrow] {
width: calc(100% - 24px);
margin: 11px;
}
ha-chip {
padding: 4px;
margin-top: 3px;
}
`]}}]}}),t);export{l as r};

View File

@ -1 +0,0 @@
import{A as o}from"./main-ad130be7.js";const a=()=>import("./c.f12697b4.js"),i=(i,l,m)=>new Promise((n=>{const r=l.cancel,s=l.confirm;o(i,"show-dialog",{dialogTag:"dialog-box",dialogImport:a,dialogParams:{...l,...m,cancel:()=>{n(!(null==m||!m.prompt)&&null),r&&r()},confirm:o=>{n(null==m||!m.prompt||o),s&&s(o)}}})})),l=(o,a)=>i(o,a),m=(o,a)=>i(o,a,{confirmation:!0}),n=(o,a)=>i(o,a,{prompt:!0});export{l as a,n as b,m as s};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
function t(t){const a=t.language||"en";return t.translationMetadata.translations[a]&&t.translationMetadata.translations[a].isRTL||!1}function a(a){return t(a)?"rtl":"ltr"}export{a,t as c};

File diff suppressed because one or more lines are too long

View File

@ -1,59 +0,0 @@
import{a as o,H as t,e as s,t as e,m as i,a0 as a,a1 as r,$ as l,aj as n,ak as h,a3 as c,ai as d,d as p,r as _,n as m}from"./main-ad130be7.js";import{c as y}from"./c.4a97632a.js";import"./c.f1291e50.js";import"./c.d262aab0.js";import{s as v}from"./c.4266acdb.js";import{f as g,r as u,a as f}from"./c.fe747ba2.js";import{u as w}from"./c.25ed1ae4.js";import"./c.5d3ce9d6.js";import"./c.82e03b89.js";import"./c.9b92f489.js";import"./c.82eccc94.js";import"./c.8e28b461.js";import"./c.3f859915.js";import"./c.0ca5587f.js";import"./c.42d6aebd.js";import"./c.710a50fc.js";let b=o([m("hacs-download-dialog")],(function(o,t){return{F:class extends t{constructor(...t){super(...t),o(this)}},d:[{kind:"field",decorators:[s()],key:"repository",value:void 0},{kind:"field",decorators:[e()],key:"_toggle",value:()=>!0},{kind:"field",decorators:[e()],key:"_installing",value:()=>!1},{kind:"field",decorators:[e()],key:"_error",value:void 0},{kind:"field",decorators:[e()],key:"_repository",value:void 0},{kind:"field",decorators:[e()],key:"_downloadRepositoryData",value:()=>({beta:!1,version:""})},{kind:"method",key:"shouldUpdate",value:function(o){return o.forEach(((o,t)=>{"hass"===t&&(this.sidebarDocked='"docked"'===window.localStorage.getItem("dockedSidebar")),"repositories"===t&&this._fetchRepository()})),o.has("sidebarDocked")||o.has("narrow")||o.has("active")||o.has("_toggle")||o.has("_error")||o.has("_repository")||o.has("_downloadRepositoryData")||o.has("_installing")}},{kind:"field",key:"_getInstallPath",value:()=>i((o=>{let t=o.local_path;return"theme"===o.category&&(t=`${t}/${o.file_name}`),t}))},{kind:"method",key:"firstUpdated",value:async function(){var o;await this._fetchRepository(),this._toggle=!1,a(this.hass,(o=>this._error=o),r.ERROR),this._downloadRepositoryData.beta=this._repository.beta,this._downloadRepositoryData.version="version"===(null===(o=this._repository)||void 0===o?void 0:o.version_or_commit)?this._repository.releases[0]:""}},{kind:"method",key:"_fetchRepository",value:async function(){this._repository=await g(this.hass,this.repository)}},{kind:"method",key:"render",value:function(){var o;if(!this.active||!this._repository)return l``;const t=this._getInstallPath(this._repository),s=[{name:"beta",selector:{boolean:{}}},{name:"version",selector:{select:{options:"version"===this._repository.version_or_commit?this._repository.releases.concat("hacs/integration"===this._repository.full_name||this._repository.hide_default_branch?[]:[this._repository.default_branch]):[],mode:"dropdown"}}}];return l`
<hacs-dialog
.active=${this.active}
.narrow=${this.narrow}
.hass=${this.hass}
.secondary=${this.secondary}
.title=${this._repository.name}
>
<div class="content">
${"version"===this._repository.version_or_commit?l`
<ha-form
.disabled=${this._toggle}
?narrow=${this.narrow}
.data=${this._downloadRepositoryData}
.schema=${s}
.computeLabel=${o=>"beta"===o.name?this.hacs.localize("dialog_download.show_beta"):this.hacs.localize("dialog_download.select_version")}
@value-changed=${this._valueChanged}
>
</ha-form>
`:""}
${this._repository.can_download?"":l`<ha-alert alert-type="error" .rtl=${y(this.hass)}>
${this.hacs.localize("confirm.home_assistant_version_not_correct",{haversion:this.hass.config.version,minversion:this._repository.homeassistant})}
</ha-alert>`}
<div class="note">
${this.hacs.localize("dialog_download.note_downloaded",{location:l`<code>'${t}'</code>`})}
${"plugin"===this._repository.category&&"storage"!==this.hacs.info.lovelace_mode?l`
<p>${this.hacs.localize("dialog_download.lovelace_instruction")}</p>
<pre>
url: ${n({repository:this._repository,skipTag:!0})}
type: module
</pre
>
`:""}
${"integration"===this._repository.category?l`<p>${this.hacs.localize("dialog_download.restart")}</p>`:""}
</div>
${null!==(o=this._error)&&void 0!==o&&o.message?l`<ha-alert alert-type="error" .rtl=${y(this.hass)}>
${this._error.message}
</ha-alert>`:""}
</div>
<mwc-button
slot="primaryaction"
?disabled=${!(this._repository.can_download&&!this._toggle&&"version"!==this._repository.version_or_commit)&&!this._downloadRepositoryData.version}
@click=${this._installRepository}
>
${this._installing?l`<ha-circular-progress active size="small"></ha-circular-progress>`:this.hacs.localize("common.download")}
</mwc-button>
</hacs-dialog>
`}},{kind:"method",key:"_valueChanged",value:async function(o){let t=!1;if(this._downloadRepositoryData.beta!==o.detail.value.beta&&(t=!0,this._toggle=!0,await h(this.hass,this.repository,o.detail.value.beta)),o.detail.value.version&&(t=!0,this._toggle=!0,await u(this.hass,this.repository,o.detail.value.version)),t){const o=await c(this.hass);await this._fetchRepository(),this.dispatchEvent(new CustomEvent("update-hacs",{detail:{repositories:o},bubbles:!0,composed:!0})),this._toggle=!1}this._downloadRepositoryData=o.detail.value}},{kind:"method",key:"_installRepository",value:async function(){var o;if(this._installing=!0,!this._repository)return;const t=this._downloadRepositoryData.version||this._repository.available_version||this._repository.default_branch;"commit"!==(null===(o=this._repository)||void 0===o?void 0:o.version_or_commit)?await f(this.hass,String(this._repository.id),t):await f(this.hass,String(this._repository.id)),this.hacs.log.debug(this._repository.category,"_installRepository"),this.hacs.log.debug(this.hacs.info.lovelace_mode,"_installRepository"),"plugin"===this._repository.category&&"storage"===this.hacs.info.lovelace_mode&&await w(this.hass,this._repository,t),this._installing=!1,this.dispatchEvent(new Event("hacs-secondary-dialog-closed",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("hacs-dialog-closed",{bubbles:!0,composed:!0})),"plugin"===this._repository.category&&v(this,{title:this.hacs.localize("common.reload"),text:l`${this.hacs.localize("dialog.reload.description")}<br />${this.hacs.localize("dialog.reload.confirm")}`,dismissText:this.hacs.localize("common.cancel"),confirmText:this.hacs.localize("common.reload"),confirm:()=>{d.location.href=d.location.href}})}},{kind:"get",static:!0,key:"styles",value:function(){return[p,_`
.note {
margin-top: 12px;
}
.lovelace {
margin-top: 8px;
}
pre {
white-space: pre-line;
user-select: all;
}
`]}}]}}),t);export{b as HacsDonwloadDialog};

View File

@ -1,176 +0,0 @@
import{a6 as t,a7 as i,a8 as a}from"./main-ad130be7.js";t({_template:i`
<style>
:host {
display: block;
position: absolute;
outline: none;
z-index: 1002;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
cursor: default;
}
#tooltip {
display: block;
outline: none;
@apply --paper-font-common-base;
font-size: 10px;
line-height: 1;
background-color: var(--paper-tooltip-background, #616161);
color: var(--paper-tooltip-text-color, white);
padding: 8px;
border-radius: 2px;
@apply --paper-tooltip;
}
@keyframes keyFrameScaleUp {
0% {
transform: scale(0.0);
}
100% {
transform: scale(1.0);
}
}
@keyframes keyFrameScaleDown {
0% {
transform: scale(1.0);
}
100% {
transform: scale(0.0);
}
}
@keyframes keyFrameFadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: var(--paper-tooltip-opacity, 0.9);
}
}
@keyframes keyFrameFadeOutOpacity {
0% {
opacity: var(--paper-tooltip-opacity, 0.9);
}
100% {
opacity: 0;
}
}
@keyframes keyFrameSlideDownIn {
0% {
transform: translateY(-2000px);
opacity: 0;
}
10% {
opacity: 0.2;
}
100% {
transform: translateY(0);
opacity: var(--paper-tooltip-opacity, 0.9);
}
}
@keyframes keyFrameSlideDownOut {
0% {
transform: translateY(0);
opacity: var(--paper-tooltip-opacity, 0.9);
}
10% {
opacity: 0.2;
}
100% {
transform: translateY(-2000px);
opacity: 0;
}
}
.fade-in-animation {
opacity: 0;
animation-delay: var(--paper-tooltip-delay-in, 500ms);
animation-name: keyFrameFadeInOpacity;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: var(--paper-tooltip-duration-in, 500ms);
animation-fill-mode: forwards;
@apply --paper-tooltip-animation;
}
.fade-out-animation {
opacity: var(--paper-tooltip-opacity, 0.9);
animation-delay: var(--paper-tooltip-delay-out, 0ms);
animation-name: keyFrameFadeOutOpacity;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: var(--paper-tooltip-duration-out, 500ms);
animation-fill-mode: forwards;
@apply --paper-tooltip-animation;
}
.scale-up-animation {
transform: scale(0);
opacity: var(--paper-tooltip-opacity, 0.9);
animation-delay: var(--paper-tooltip-delay-in, 500ms);
animation-name: keyFrameScaleUp;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: var(--paper-tooltip-duration-in, 500ms);
animation-fill-mode: forwards;
@apply --paper-tooltip-animation;
}
.scale-down-animation {
transform: scale(1);
opacity: var(--paper-tooltip-opacity, 0.9);
animation-delay: var(--paper-tooltip-delay-out, 500ms);
animation-name: keyFrameScaleDown;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: var(--paper-tooltip-duration-out, 500ms);
animation-fill-mode: forwards;
@apply --paper-tooltip-animation;
}
.slide-down-animation {
transform: translateY(-2000px);
opacity: 0;
animation-delay: var(--paper-tooltip-delay-out, 500ms);
animation-name: keyFrameSlideDownIn;
animation-iteration-count: 1;
animation-timing-function: cubic-bezier(0.0, 0.0, 0.2, 1);
animation-duration: var(--paper-tooltip-duration-out, 500ms);
animation-fill-mode: forwards;
@apply --paper-tooltip-animation;
}
.slide-down-animation-out {
transform: translateY(0);
opacity: var(--paper-tooltip-opacity, 0.9);
animation-delay: var(--paper-tooltip-delay-out, 500ms);
animation-name: keyFrameSlideDownOut;
animation-iteration-count: 1;
animation-timing-function: cubic-bezier(0.4, 0.0, 1, 1);
animation-duration: var(--paper-tooltip-duration-out, 500ms);
animation-fill-mode: forwards;
@apply --paper-tooltip-animation;
}
.cancel-animation {
animation-delay: -30s !important;
}
/* Thanks IE 10. */
.hidden {
display: none !important;
}
</style>
<div id="tooltip" class="hidden">
<slot></slot>
</div>
`,is:"paper-tooltip",hostAttributes:{role:"tooltip",tabindex:-1},properties:{for:{type:String,observer:"_findTarget"},manualMode:{type:Boolean,value:!1,observer:"_manualModeChanged"},position:{type:String,value:"bottom"},fitToVisibleBounds:{type:Boolean,value:!1},offset:{type:Number,value:14},marginTop:{type:Number,value:14},animationDelay:{type:Number,value:500,observer:"_delayChange"},animationEntry:{type:String,value:""},animationExit:{type:String,value:""},animationConfig:{type:Object,value:function(){return{entry:[{name:"fade-in-animation",node:this,timing:{delay:0}}],exit:[{name:"fade-out-animation",node:this}]}}},_showing:{type:Boolean,value:!1}},listeners:{webkitAnimationEnd:"_onAnimationEnd"},get target(){var t=a(this).parentNode,i=a(this).getOwnerRoot();return this.for?a(i).querySelector("#"+this.for):t.nodeType==Node.DOCUMENT_FRAGMENT_NODE?i.host:t},attached:function(){this._findTarget()},detached:function(){this.manualMode||this._removeListeners()},playAnimation:function(t){"entry"===t?this.show():"exit"===t&&this.hide()},cancelAnimation:function(){this.$.tooltip.classList.add("cancel-animation")},show:function(){if(!this._showing){if(""===a(this).textContent.trim()){for(var t=!0,i=a(this).getEffectiveChildNodes(),n=0;n<i.length;n++)if(""!==i[n].textContent.trim()){t=!1;break}if(t)return}this._showing=!0,this.$.tooltip.classList.remove("hidden"),this.$.tooltip.classList.remove("cancel-animation"),this.$.tooltip.classList.remove(this._getAnimationType("exit")),this.updatePosition(),this._animationPlaying=!0,this.$.tooltip.classList.add(this._getAnimationType("entry"))}},hide:function(){if(this._showing){if(this._animationPlaying)return this._showing=!1,void this._cancelAnimation();this._onAnimationFinish(),this._showing=!1,this._animationPlaying=!0}},updatePosition:function(){if(this._target&&this.offsetParent){var t=this.offset;14!=this.marginTop&&14==this.offset&&(t=this.marginTop);var i,a,n=this.offsetParent.getBoundingClientRect(),e=this._target.getBoundingClientRect(),o=this.getBoundingClientRect(),s=(e.width-o.width)/2,r=(e.height-o.height)/2,l=e.left-n.left,p=e.top-n.top;switch(this.position){case"top":i=l+s,a=p-o.height-t;break;case"bottom":i=l+s,a=p+e.height+t;break;case"left":i=l-o.width-t,a=p+r;break;case"right":i=l+e.width+t,a=p+r}this.fitToVisibleBounds?(n.left+i+o.width>window.innerWidth?(this.style.right="0px",this.style.left="auto"):(this.style.left=Math.max(0,i)+"px",this.style.right="auto"),n.top+a+o.height>window.innerHeight?(this.style.bottom=n.height-p+t+"px",this.style.top="auto"):(this.style.top=Math.max(-n.top,a)+"px",this.style.bottom="auto")):(this.style.left=i+"px",this.style.top=a+"px")}},_addListeners:function(){this._target&&(this.listen(this._target,"mouseenter","show"),this.listen(this._target,"focus","show"),this.listen(this._target,"mouseleave","hide"),this.listen(this._target,"blur","hide"),this.listen(this._target,"tap","hide")),this.listen(this.$.tooltip,"animationend","_onAnimationEnd"),this.listen(this,"mouseenter","hide")},_findTarget:function(){this.manualMode||this._removeListeners(),this._target=this.target,this.manualMode||this._addListeners()},_delayChange:function(t){500!==t&&this.updateStyles({"--paper-tooltip-delay-in":t+"ms"})},_manualModeChanged:function(){this.manualMode?this._removeListeners():this._addListeners()},_cancelAnimation:function(){this.$.tooltip.classList.remove(this._getAnimationType("entry")),this.$.tooltip.classList.remove(this._getAnimationType("exit")),this.$.tooltip.classList.remove("cancel-animation"),this.$.tooltip.classList.add("hidden")},_onAnimationFinish:function(){this._showing&&(this.$.tooltip.classList.remove(this._getAnimationType("entry")),this.$.tooltip.classList.remove("cancel-animation"),this.$.tooltip.classList.add(this._getAnimationType("exit")))},_onAnimationEnd:function(){this._animationPlaying=!1,this._showing||(this.$.tooltip.classList.remove(this._getAnimationType("exit")),this.$.tooltip.classList.add("hidden"))},_getAnimationType:function(t){if("entry"===t&&""!==this.animationEntry)return this.animationEntry;if("exit"===t&&""!==this.animationExit)return this.animationExit;if(this.animationConfig[t]&&"string"==typeof this.animationConfig[t][0].name){if(this.animationConfig[t][0].timing&&this.animationConfig[t][0].timing.delay&&0!==this.animationConfig[t][0].timing.delay){var i=this.animationConfig[t][0].timing.delay;"entry"===t?this.updateStyles({"--paper-tooltip-delay-in":i+"ms"}):"exit"===t&&this.updateStyles({"--paper-tooltip-delay-out":i+"ms"})}return this.animationConfig[t][0].name}},_removeListeners:function(){this._target&&(this.unlisten(this._target,"mouseenter","show"),this.unlisten(this._target,"focus","show"),this.unlisten(this._target,"mouseleave","hide"),this.unlisten(this._target,"blur","hide"),this.unlisten(this._target,"tap","hide")),this.unlisten(this.$.tooltip,"animationend","_onAnimationEnd"),this.unlisten(this,"mouseenter","hide")}});

View File

@ -1 +0,0 @@
const e=()=>{const e={},r=new URLSearchParams(location.search);for(const[n,t]of r.entries())e[n]=t;return e},r=e=>{const r=new URLSearchParams;return Object.entries(e).forEach((([e,n])=>{r.append(e,n)})),r.toString()};export{r as c,e};

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
import{a as t,h as e,e as r,$ as n,ah as i,ai as a,r as o,n as s}from"./main-ad130be7.js";t([s("hacs-link")],(function(t,e){return{F:class extends e{constructor(...e){super(...e),t(this)}},d:[{kind:"field",decorators:[r({type:Boolean})],key:"newtab",value:()=>!1},{kind:"field",decorators:[r({type:Boolean})],key:"parent",value:()=>!1},{kind:"field",decorators:[r()],key:"title",value:()=>""},{kind:"field",decorators:[r()],key:"url",value:void 0},{kind:"method",key:"render",value:function(){return n`<span title=${this.title||this.url} @click=${this._open}><slot></slot></span>`}},{kind:"method",key:"_open",value:function(){var t;if(this.url.startsWith("/")&&!this.newtab)return void i(this.url,{replace:!0});const e=null===(t=this.url)||void 0===t?void 0:t.startsWith("http");let r="",n="_blank";e&&(r="noreferrer=true"),e||this.newtab||(n="_blank"),e||this.parent||(n="_parent"),a.open(this.url,n,r)}},{kind:"get",static:!0,key:"styles",value:function(){return o`
span {
cursor: pointer;
color: var(--hcv-text-color-link);
text-decoration: var(--hcv-text-decoration-link);
}
`}}]}}),e);

View File

@ -1 +0,0 @@
var a=[];export{a as default};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function o(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function t(e,o){return e(o={exports:{}},o.exports),o.exports}function n(e){return e&&e.default||e}export{e as a,t as c,n as g,o as u};

View File

@ -1,121 +0,0 @@
import{_ as e,n as t,a as i,H as a,e as s,b as r,m as o,$ as l,o as c,c as d,s as n,d as h,r as u}from"./main-ad130be7.js";import"./c.82eccc94.js";import{s as p,S as f,a as m}from"./c.42d6aebd.js";import"./c.f1291e50.js";import"./c.9b92f489.js";import"./c.11ad1623.js";import{f as v}from"./c.3243a8b0.js";import{b as g}from"./c.0a1cf8d0.js";import"./c.a5f69ed4.js";import"./c.82e03b89.js";import"./c.8e28b461.js";import"./c.3f859915.js";import"./c.710a50fc.js";let y=class extends f{};y.styles=[p],y=e([t("mwc-select")],y);const _=["stars","last_updated","name"];let k=i([t("hacs-add-repository-dialog")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[s({attribute:!1})],key:"filters",value:()=>[]},{kind:"field",decorators:[s({type:Number})],key:"_load",value:()=>30},{kind:"field",decorators:[s({type:Number})],key:"_top",value:()=>0},{kind:"field",decorators:[s()],key:"_searchInput",value:()=>""},{kind:"field",decorators:[s()],key:"_sortBy",value:()=>_[0]},{kind:"field",decorators:[s()],key:"section",value:void 0},{kind:"method",key:"shouldUpdate",value:function(e){return e.forEach(((e,t)=>{"hass"===t&&(this.sidebarDocked='"docked"'===window.localStorage.getItem("dockedSidebar"))})),e.has("narrow")||e.has("filters")||e.has("active")||e.has("_searchInput")||e.has("_load")||e.has("_sortBy")}},{kind:"field",key:"_repositoriesInActiveCategory",value(){return(e,t)=>null==e?void 0:e.filter((e=>{var i,a;return!e.installed&&(null===(i=this.hacs.sections)||void 0===i||null===(a=i.find((e=>e.id===this.section)).categories)||void 0===a?void 0:a.includes(e.category))&&!e.installed&&(null==t?void 0:t.includes(e.category))}))}},{kind:"method",key:"firstUpdated",value:async function(){var e;if(this.addEventListener("filter-change",(e=>this._updateFilters(e))),0===(null===(e=this.filters)||void 0===e?void 0:e.length)){var t;const e=null===(t=r(this.hacs.language,this.route))||void 0===t?void 0:t.categories;null==e||e.filter((e=>{var t;return null===(t=this.hacs.info)||void 0===t?void 0:t.categories.includes(e)})).forEach((e=>{this.filters.push({id:e,value:e,checked:!0})})),this.requestUpdate("filters")}}},{kind:"method",key:"_updateFilters",value:function(e){const t=this.filters.find((t=>t.id===e.detail.id));this.filters.find((e=>e.id===t.id)).checked=!t.checked,this.requestUpdate("filters")}},{kind:"field",key:"_filterRepositories",value:()=>o(v)},{kind:"method",key:"render",value:function(){var e;if(!this.active)return l``;this._searchInput=window.localStorage.getItem("hacs-search")||"";let t=this._filterRepositories(this._repositoriesInActiveCategory(this.repositories,null===(e=this.hacs.info)||void 0===e?void 0:e.categories),this._searchInput);return 0!==this.filters.length&&(t=t.filter((e=>{var t;return null===(t=this.filters.find((t=>t.id===e.category)))||void 0===t?void 0:t.checked}))),l`
<hacs-dialog
.active=${this.active}
.hass=${this.hass}
.title=${this.hacs.localize("dialog_add_repo.title")}
hideActions
scrimClickAction
maxWidth
>
<div class="searchandfilter" ?narrow=${this.narrow}>
<search-input
.hass=${this.hass}
.label=${this.hacs.localize("search.placeholder")}
.filter=${this._searchInput}
@value-changed=${this._inputValueChanged}
?narrow=${this.narrow}
></search-input>
<mwc-select
?narrow=${this.narrow}
.label=${this.hacs.localize("dialog_add_repo.sort_by")}
.value=${this._sortBy}
@selected=${e=>this._sortBy=e.currentTarget.value}
@closed=${m}
>
${_.map((e=>l`<mwc-list-item .value=${e}>
${this.hacs.localize(`dialog_add_repo.sort_by_values.${e}`)||e}
</mwc-list-item>`))}
</mwc-select>
</div>
${this.filters.length>1?l`<div class="filters">
<hacs-filter .hacs=${this.hacs} .filters="${this.filters}"></hacs-filter>
</div>`:""}
<div class=${c({content:!0,narrow:this.narrow})} @scroll=${this._loadMore}>
<mwc-list>
${0===t.length?l`<ha-alert>${this.hacs.localize("dialog_add_repo.no_match")}</ha-alert>`:t.sort(((e,t)=>"name"===this._sortBy?e.name.toLocaleLowerCase()<t.name.toLocaleLowerCase()?-1:1:e[this._sortBy]>t[this._sortBy]?-1:1)).slice(0,this._load).map((e=>l`<ha-clickable-list-item
graphic=${this.narrow?"":"avatar"}
twoline
@click=${()=>this.active=!1}
href="/hacs/repository/${e.id}"
.hasMeta=${!this.narrow&&"integration"!==e.category}
>
${this.narrow?"":"integration"===e.category?l`
<img
loading="lazy"
.src=${g({domain:e.domain,darkOptimized:this.hass.themes.darkMode,type:"icon"})}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
slot="graphic"
/>
`:l`
<ha-svg-icon
slot="graphic"
path="${d}"
style="padding-left: 0; height: 40px; width: 40px;"
>
</ha-svg-icon>
`}
<span>${e.name}</span>
<span slot="secondary">${e.description}</span>
<ha-chip slot="meta">
${this.hacs.localize(`common.${e.category}`)}
</ha-chip>
</ha-clickable-list-item>`))}
</mwc-list>
</div>
</hacs-dialog>
`}},{kind:"method",key:"_loadMore",value:function(e){const t=e.target.scrollTop;t>=this._top?this._load+=1:this._load-=1,this._top=t}},{kind:"method",key:"_inputValueChanged",value:function(e){this._searchInput=e.detail.value,window.localStorage.setItem("hacs-search",this._searchInput)}},{kind:"method",key:"_onImageLoad",value:function(e){e.target.style.visibility="initial"}},{kind:"method",key:"_onImageError",value:function(e){var t;if(null!==(t=e.target)&&void 0!==t&&t.outerHTML)try{e.target.outerHTML=`<ha-svg-icon path="${d}" slot="graphic"></ha-svg-icon>`}catch(e){}}},{kind:"get",static:!0,key:"styles",value:function(){return[n,h,u`
.content {
width: 100%;
overflow: auto;
max-height: 70vh;
}
.filter {
margin-top: -12px;
display: flex;
width: 200px;
float: right;
}
.list {
margin-top: 16px;
width: 1024px;
max-width: 100%;
}
search-input {
display: block;
float: left;
width: 75%;
}
search-input[narrow],
mwc-select[narrow] {
width: 100%;
margin: 4px 0;
}
.filters {
width: 100%;
display: flex;
}
hacs-filter {
width: 100%;
margin-left: -32px;
}
.searchandfilter {
display: flex;
justify-content: space-between;
align-items: self-end;
}
.searchandfilter[narrow] {
flex-direction: column;
}
ha-chip {
margin-left: -52px;
}
`]}}]}}),a);export{k as HacsAddRepositoryDialog};

View File

@ -1,178 +0,0 @@
import{a as o,h as t,e,$ as i,w as a,r as s,n as r,t as n,L as h,N as l,ai as c,a2 as d,a3 as p,m as u,c as y,aN as m,aO as f,aP as v,E as _,aQ as g,z as b,aR as k,aS as w,aT as $,aU as x,aV as z,aW as j,ah as R,am as L,ar as S,as as F,aX as I,d as P}from"./main-ad130be7.js";import"./c.bc5a73e9.js";import{e as C}from"./c.50bfd408.js";import"./c.97b7c4b0.js";import{r as E}from"./c.4204ca09.js";import{g as T}from"./c.f2bb3724.js";import{s as U}from"./c.4266acdb.js";import"./c.a5f69ed4.js";import{f as D}from"./c.fe747ba2.js";import{m as K}from"./c.f6611997.js";import"./c.2d5ed670.js";import"./c.9b92f489.js";import"./c.82eccc94.js";import"./c.8e28b461.js";import"./c.4feb0cb8.js";import"./c.0ca5587f.js";import"./c.5d3ce9d6.js";import"./c.743a15a1.js";o([r("hass-subpage")],(function(o,t){return{F:class extends t{constructor(...t){super(...t),o(this)}},d:[{kind:"field",decorators:[e({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[e()],key:"header",value:void 0},{kind:"field",decorators:[e({type:Boolean,attribute:"main-page"})],key:"mainPage",value:()=>!1},{kind:"field",decorators:[e({type:String,attribute:"back-path"})],key:"backPath",value:void 0},{kind:"field",decorators:[e({type:Boolean,reflect:!0})],key:"narrow",value:()=>!1},{kind:"field",decorators:[e({type:Boolean})],key:"supervisor",value:()=>!1},{kind:"field",decorators:[E(".content")],key:"_savedScrollPos",value:void 0},{kind:"method",key:"render",value:function(){var o;return i`
<div class="toolbar">
${this.mainPage||null!==(o=history.state)&&void 0!==o&&o.root?i`
<ha-menu-button
.hassio=${this.supervisor}
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`:this.backPath?i`
<a href=${this.backPath}>
<ha-icon-button-arrow-prev
.hass=${this.hass}
></ha-icon-button-arrow-prev>
</a>
`:i`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
<div class="main-title">${this.header}</div>
<slot name="toolbar-icon"></slot>
</div>
<div class="content" @scroll=${this._saveScrollPos}><slot></slot></div>
`}},{kind:"method",decorators:[a({passive:!0})],key:"_saveScrollPos",value:function(o){this._savedScrollPos=o.target.scrollTop}},{kind:"method",key:"_backTapped",value:function(){history.back()}},{kind:"get",static:!0,key:"styles",value:function(){return s`
:host {
display: block;
height: 100%;
background-color: var(--primary-background-color);
}
:host([narrow]) {
width: 100%;
position: fixed;
}
.toolbar {
display: flex;
align-items: center;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
.toolbar a {
color: var(--sidebar-text-color);
text-decoration: none;
}
ha-menu-button,
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
pointer-events: auto;
color: var(--sidebar-icon-color);
}
.main-title {
margin: 0 0 0 24px;
line-height: 20px;
flex-grow: 1;
}
.content {
position: relative;
width: 100%;
height: calc(100% - 1px - var(--header-height));
overflow-y: auto;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
`}}]}}),t);let M=o([r("hacs-repository-panel")],(function(o,t){class a extends t{constructor(...t){super(...t),o(this)}}return{F:a,d:[{kind:"field",decorators:[e({attribute:!1})],key:"hacs",value:void 0},{kind:"field",decorators:[e({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[e({attribute:!1})],key:"narrow",value:void 0},{kind:"field",decorators:[e({attribute:!1})],key:"isWide",value:void 0},{kind:"field",decorators:[e({attribute:!1})],key:"route",value:void 0},{kind:"field",decorators:[e({attribute:!1})],key:"_repository",value:void 0},{kind:"field",decorators:[n()],key:"_error",value:void 0},{kind:"method",key:"connectedCallback",value:function(){h(l(a.prototype),"connectedCallback",this).call(this),document.body.addEventListener("keydown",this._generateMyLink)}},{kind:"method",key:"disconnectedCallback",value:function(){h(l(a.prototype),"disconnectedCallback",this).call(this),document.body.removeEventListener("keydown",this._generateMyLink)}},{kind:"field",key:"_generateMyLink",value(){return o=>{if(!(o.ctrlKey||o.shiftKey||o.metaKey||o.altKey)&&"m"===o.key&&c.location.pathname.startsWith("/hacs/repository/")){if(!this._repository)return;const o=new URLSearchParams({redirect:"hacs_repository",owner:this._repository.full_name.split("/")[0],repository:this._repository.full_name.split("/")[1],category:this._repository.category});window.open(`https://my.home-assistant.io/create-link/?${o.toString()}`,"_blank")}}}},{kind:"method",key:"firstUpdated",value:async function(o){h(l(a.prototype),"firstUpdated",this).call(this,o);const t=C();if(Object.entries(t).length){let o;const e=`${t.owner}/${t.repository}`;if(o=this.hacs.repositories.find((o=>o.full_name.toLocaleLowerCase()===e.toLocaleLowerCase())),!o&&t.category){if(!await U(this,{title:this.hacs.localize("my.add_repository_title"),text:this.hacs.localize("my.add_repository_description",{repository:e}),confirmText:this.hacs.localize("common.add"),dismissText:this.hacs.localize("common.cancel")}))return void(this._error=this.hacs.localize("my.repository_not_found",{repository:e}));try{await d(this.hass,e,t.category),this.hacs.repositories=await p(this.hass),o=this.hacs.repositories.find((o=>o.full_name.toLocaleLowerCase()===e.toLocaleLowerCase()))}catch(o){return void(this._error=o)}}o?this._fetchRepository(String(o.id)):this._error=this.hacs.localize("my.repository_not_found",{repository:e})}else{const o=this.route.path.indexOf("/",1),t=this.route.path.substr(o+1);if(!t)return void(this._error="Missing repositoryId from route");this._fetchRepository(t)}}},{kind:"method",key:"updated",value:function(o){h(l(a.prototype),"updated",this).call(this,o),o.has("repositories")&&this._repository&&this._fetchRepository()}},{kind:"method",key:"_fetchRepository",value:async function(o){try{this._repository=await D(this.hass,o||String(this._repository.id))}catch(o){this._error=null==o?void 0:o.message}}},{kind:"field",key:"_getAuthors",value:()=>u((o=>{const t=[];if(!o.authors)return t;if(o.authors.forEach((o=>t.push(o.replace("@","")))),0===t.length){const e=o.full_name.split("/")[0];if(["custom-cards","custom-components","home-assistant-community-themes"].includes(e))return t;t.push(e)}return t}))},{kind:"method",key:"render",value:function(){if(this._error)return i`<hass-error-screen .error=${this._error}></hass-error-screen>`;if(!this._repository)return i`<hass-loading-screen></hass-loading-screen>`;const o=this._getAuthors(this._repository);return i`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.header=${this._repository.name}
hasFab
>
<ha-icon-overflow-menu
slot="toolbar-icon"
narrow
.hass=${this.hass}
.items=${[{path:y,label:this.hacs.localize("common.repository"),action:()=>c.open(`https://github.com/${this._repository.full_name}`,"_blank","noreferrer=true")},{path:m,label:this.hacs.localize("repository_card.update_information"),action:()=>this._refreshReopsitoryInfo()},{path:f,label:this.hacs.localize("repository_card.redownload"),action:()=>this._downloadRepositoryDialog(),hideForUninstalled:!0},{category:"plugin",hideForUninstalled:!0,path:v,label:this.hacs.localize("repository_card.open_source"),action:()=>c.open(`/hacsfiles/${this._repository.local_path.split("/").pop()}/${this._repository.file_name}`,"_blank","noreferrer=true")},{path:_,label:this.hacs.localize("repository_card.open_issue"),action:()=>c.open(`https://github.com/${this._repository.full_name}/issues`,"_blank","noreferrer=true")},{hideForId:"172733314",path:g,label:this.hacs.localize("repository_card.report"),hideForUninstalled:!0,action:()=>c.open(`https://github.com/hacs/integration/issues/new?assignees=ludeeus&labels=flag&template=removal.yml&repo=${this._repository.full_name}&title=Request for removal of ${this._repository.full_name}`,"_blank","noreferrer=true")},{hideForId:"172733314",hideForUninstalled:!0,path:b,label:this.hacs.localize("common.remove"),action:()=>this._removeRepositoryDialog()}].filter((o=>(!o.category||this._repository.category===o.category)&&(!o.hideForId||String(this._repository.id)!==o.hideForId)&&(!o.hideForUninstalled||this._repository.installed_version)))}
>
</ha-icon-overflow-menu>
<div class="content">
<div class="chips">
${this._repository.installed?i`
<ha-chip title="${this.hacs.localize("dialog_info.version_installed")}" hasIcon>
<ha-svg-icon slot="icon" .path=${k}></ha-svg-icon>
${this._repository.installed_version}
</ha-chip>
`:""}
${o?o.map((o=>i`<hacs-link .url="https://github.com/${o}">
<ha-chip title="${this.hacs.localize("dialog_info.author")}" hasIcon>
<ha-svg-icon slot="icon" .path=${w}></ha-svg-icon>
@${o}
</ha-chip>
</hacs-link>`)):""}
${this._repository.downloads?i` <ha-chip hasIcon title="${this.hacs.localize("dialog_info.downloads")}">
<ha-svg-icon slot="icon" .path=${$}></ha-svg-icon>
${this._repository.downloads}
</ha-chip>`:""}
<ha-chip title="${this.hacs.localize("dialog_info.stars")}" hasIcon>
<ha-svg-icon slot="icon" .path=${x}></ha-svg-icon>
${this._repository.stars}
</ha-chip>
<hacs-link .url="https://github.com/${this._repository.full_name}/issues">
<ha-chip title="${this.hacs.localize("dialog_info.open_issues")}" hasIcon>
<ha-svg-icon slot="icon" .path=${z}></ha-svg-icon>
${this._repository.issues}
</ha-chip>
</hacs-link>
</div>
${K.html(this._repository.additional_info||this.hacs.localize("dialog_info.no_info"),this._repository)}
</div>
${this._repository.installed_version?"":i`<ha-fab
.label=${this.hacs.localize("common.download")}
.extended=${!this.narrow}
@click=${this._downloadRepositoryDialog}
>
<ha-svg-icon slot="icon" .path=${j}></ha-svg-icon>
</ha-fab>`}
</hass-subpage>
`}},{kind:"method",key:"_downloadRepositoryDialog",value:function(){this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"download",repository:this._repository.id},bubbles:!0,composed:!0}))}},{kind:"method",key:"_removeRepositoryDialog",value:async function(){if("integration"===this._repository.category&&this._repository.config_flow){if((await T(this.hass)).some((o=>o.domain===this._repository.domain))){if(await U(this,{title:this.hacs.localize("dialog.configured.title"),text:this.hacs.localize("dialog.configured.message",{name:this._repository.name}),dismissText:this.hacs.localize("common.ignore"),confirmText:this.hacs.localize("common.navigate"),confirm:()=>{R("/config/integrations",{replace:!0})}}))return}}this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"progress",title:this.hacs.localize("dialog.remove.title"),confirmText:this.hacs.localize("dialog.remove.title"),content:this.hacs.localize("dialog.remove.message",{name:this._repository.name}),confirm:async()=>{await this._repositoryRemove()}},bubbles:!0,composed:!0}))}},{kind:"method",key:"_repositoryRemove",value:async function(){var o;if("plugin"===this._repository.category&&"yaml"!==(null===(o=this.hacs.info)||void 0===o?void 0:o.lovelace_mode)){(await L(this.hass)).filter((o=>o.url.startsWith(`/hacsfiles/${this._repository.full_name.split("/")[1]}/${this._repository.file_name}`))).forEach((async o=>{await S(this.hass,String(o.id))}))}await F(this.hass,String(this._repository.id)),history.back()}},{kind:"method",key:"_refreshReopsitoryInfo",value:async function(){await I(this.hass,String(this._repository.id))}},{kind:"get",static:!0,key:"styles",value:function(){return[P,s`
hass-loading-screen {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
height: 100vh;
}
hass-subpage {
position: absolute;
width: 100vw;
}
ha-svg-icon {
color: var(--hcv-text-color-on-background);
}
ha-fab {
position: fixed;
float: right;
right: calc(18px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
ha-fab.rtl {
float: left;
right: auto;
left: calc(18px + env(safe-area-inset-left));
}
.content {
padding: 12px;
margin-bottom: 64px;
}
.chips {
display: flex;
flex-wrap: wrap;
padding-bottom: 8px;
gap: 4px;
}
@media all and (max-width: 500px) {
.content {
margin: 8px 4px 64px;
}
}
`]}}]}}),t);export{M as HacsRepositoryPanel};

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