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 UID=1000
GID=1004 GID=1004
DIR=/srv/docker DIR=/srv/docker
DIR_LOCAL=/opt/docker_data
DOMAIN=milvert.com DOMAIN=milvert.com
DOMAIN_PEEK=peekskog.se DOMAIN_PEEK=peekskog.se
@ -51,3 +52,10 @@ VW_DB_NAME='vwfriend'
VW_DB_USER='vwfriend' VW_DB_USER='vwfriend'
VW_DB_PASSWORD='icCJ8iwKJBeRBg' VW_DB_PASSWORD='icCJ8iwKJBeRBg'
ADDITIONAL_PARAMETERS=-vv --mqttbroker mqtt --mqttport 1883 -mu simon -mp bajsa123 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 port: 53
anonymize_client_ip: false anonymize_client_ip: false
ratelimit: 20 ratelimit: 20
ratelimit_subnet_len_ipv4: 24
ratelimit_subnet_len_ipv6: 56
ratelimit_whitelist: [] ratelimit_whitelist: []
refuse_any: true refuse_any: true
upstream_dns: 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: "" upstream_dns_file: ""
bootstrap_dns: bootstrap_dns:
- 9.9.9.10 - 9.9.9.10
- 149.112.112.10 - 149.112.112.10
- 2620:fe::10 - 2620:fe::10
- 2620:fe::fe:10 - 2620:fe::fe:10
fallback_dns: [] fallback_dns:
all_servers: false - 1.1.1.2
fastest_addr: false upstream_mode: load_balance
fastest_timeout: 1s fastest_timeout: 1s
allowed_clients: [] allowed_clients: []
disallowed_clients: [] disallowed_clients: []
@ -59,12 +63,14 @@ dns:
bootstrap_prefer_ipv6: false bootstrap_prefer_ipv6: false
upstream_timeout: 10s upstream_timeout: 10s
private_networks: [] private_networks: []
use_private_ptr_resolvers: true use_private_ptr_resolvers: false
local_ptr_upstreams: [] local_ptr_upstreams: []
use_dns64: false use_dns64: false
dns64_prefixes: [] dns64_prefixes: []
serve_http3: false serve_http3: false
use_http3_upstreams: false use_http3_upstreams: false
serve_plain_dns: true
hostsfile_enabled: true
tls: tls:
enabled: false enabled: false
server_name: "" server_name: ""
@ -81,12 +87,14 @@ tls:
private_key_path: "" private_key_path: ""
strict_sni_check: false strict_sni_check: false
querylog: querylog:
dir_path: ""
ignored: [] ignored: []
interval: 2160h interval: 2160h
size_memory: 1000 size_memory: 1000
enabled: true enabled: true
file_enabled: true file_enabled: true
statistics: statistics:
dir_path: ""
ignored: [] ignored: []
interval: 24h interval: 24h
enabled: true enabled: true
@ -161,6 +169,7 @@ clients:
hosts: true hosts: true
persistent: [] persistent: []
log: log:
enabled: true
file: "" file: ""
max_backups: 0 max_backups: 0
max_size: 100 max_size: 100
@ -172,4 +181,4 @@ os:
group: "" group: ""
user: "" user: ""
rlimit_nofile: 0 rlimit_nofile: 0
schema_version: 27 schema_version: 28

View File

@ -1,9 +1,9 @@
############################################################### ###############################################################
# Authelia configuration # # Authelia configuration #
############################################################### ###############################################################
server:
address: 'tcp://:9091'
server.host: 0.0.0.0
server.port: 9091
log: log:
level: info level: info
@ -14,6 +14,13 @@ log:
# https://docs.authelia.com/configuration/miscellaneous.html#default-redirection-url # https://docs.authelia.com/configuration/miscellaneous.html#default-redirection-url
default_redirection_url: https://authelia.milvert.com 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: totp:
issuer: authelia.com issuer: authelia.com
period: 30 period: 30
@ -56,6 +63,10 @@ access_control:
policy: two_factor policy: two_factor
- domain: "milvert.com" - domain: "milvert.com"
policy: two_factor policy: two_factor
- domain:
- 'uptime.example.com'
subject: 'oauth2:client:uptime-kuma'
policy: 'one_factor'
session: session:
name: authelia_session name: authelia_session
@ -85,10 +96,11 @@ storage:
#path: /config/db.sqlite3 #path: /config/db.sqlite3
postgres: postgres:
# MySQL allows running multiple authelia instances. Create database and enter details below. # MySQL allows running multiple authelia instances. Create database and enter details below.
host: postgres address: 'tcp://postgres:5432'
port: 5432 #host: postgres
database: authelia #port: 5432
username: authelia database: 'authelia'
username: 'authelia'
# Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html # 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 # 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. # For testing purpose, notifications can be sent in a file. Be sure map the volume in docker-compose.
filesystem: filesystem:
filename: /tmp/authelia/notification.txt 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: labels:
- diun.enable=true - diun.enable=true
- "traefik.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.middlewares=webdb-mid"
- "traefik.http.routers.webdb-secure.entrypoints=web-secure" - "traefik.http.routers.webdb-secure.entrypoints=web-secure"
- "traefik.http.routers.webdb-secure.rule=Host(`webdb.${DOMAIN}`)" - "traefik.http.routers.webdb-secure.rule=Host(`webdb.${DOMAIN}`)"

View File

@ -3,7 +3,7 @@ version: '3'
services: services:
node-red: 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 # image: nodered/node-red-dev:3.0.0-beta.4-14
container_name: "node-red" container_name: "node-red"
@ -57,7 +57,7 @@ services:
- "traefik.enable=false" - "traefik.enable=false"
- "traefik.http.services.landet_domo-service.loadbalancer.server.port=8080" - "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.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.certresolver=milvert_dns"
- "traefik.http.routers.landet_domo-secure.tls=true" - "traefik.http.routers.landet_domo-secure.tls=true"

View File

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

View File

@ -11,7 +11,9 @@ services:
max-file: "3" max-file: "3"
networks: networks:
- backend - backend
image: koenkk/zigbee2mqtt:1.33.2 ports:
- "8088:8080"
image: koenkk/zigbee2mqtt:2.1
restart: always restart: always
volumes: volumes:
- ./zigbee_home_2:/app/data - ./zigbee_home_2:/app/data
@ -41,7 +43,7 @@ services:
max-file: "5" max-file: "5"
networks: networks:
- backend - backend
image: koenkk/zigbee2mqtt:1.33.2 image: koenkk/zigbee2mqtt:2.1
restart: always restart: always
volumes: volumes:
- ${DIR}/zigbee2matt:/app/data - ${DIR}/zigbee2matt:/app/data
@ -65,7 +67,7 @@ services:
influx: influx:
image: influxdb:2.6 image: influxdb:2.7
container_name: influxdb container_name: influxdb
logging: logging:
driver: "json-file" driver: "json-file"
@ -136,7 +138,7 @@ services:
#- "traefik.http.routers.influx-secure.middlewares=localNetwork@file" #- "traefik.http.routers.influx-secure.middlewares=localNetwork@file"
gitea: gitea:
image: gitea/gitea:1.17 image: gitea/gitea:1.21
container_name: gitea container_name: gitea
logging: logging:
driver: "json-file" driver: "json-file"
@ -171,7 +173,7 @@ services:
mqtt: mqtt:
# image: eclipse-mosquitto:1.6.13 # image: eclipse-mosquitto:1.6.13
image: eclipse-mosquitto:2.0.17 image: eclipse-mosquitto:2.0.18
container_name: mqtt container_name: mqtt
logging: logging:
driver: "json-file" driver: "json-file"
@ -248,71 +250,43 @@ services:
labels: labels:
- diun.enable=true - 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: #pihole:
image: pihole/pihole:2023.03.1 #image: pihole/pihole:2023.03.1
container_name: pihole #container_name: pihole
ports: #ports:
- "53:53/tcp" #- "53:53/tcp"
- "53:53/udp" #- "53:53/udp"
- "8001:80" #- "8001:80"
dns: #dns:
- 127.0.0.1 #- 127.0.0.1
- 9.9.9.9 #- 9.9.9.9
environment: #environment:
- TZ=${TZ} #- TZ=${TZ}
- WEBPASSWORD=${PIHOLE_PW} #- WEBPASSWORD=${PIHOLE_PW}
- ServerIP=10.0.201 #- ServerIP=10.0.201
- PIHOLE_DNS_=127.0.0.1;9.9.9.9;149.112.112.112;1.1.1.1 #- PIHOLE_DNS_=127.0.0.1;9.9.9.9;149.112.112.112;1.1.1.1
- DNSSEC='true' #- DNSSEC='true'
- VIRTUAL_HOST=pihole.milvert.com # Same as port traefik config ##- VIRTUAL_HOST=pihole.milvert.com # Same as port traefik config
- WEBTHEME=default-dark #- WEBTHEME=default-dark
- PIHOLE_DOMAIN=milvert.com #- PIHOLE_DOMAIN=milvert.com
volumes: #volumes:
- ${DIR}/pihole/etc:/etc/pihole #- ${DIR}/pihole/etc:/etc/pihole
- ${DIR}/pihole/dns:/etc/dnsmasq.d #- ${DIR}/pihole/dns:/etc/dnsmasq.d
restart: unless-stopped #restart: unless-stopped
networks: #networks:
backend: #backend:
labels: #labels:
- diun.enable=true #- diun.enable=true
- "traefik.enable=true" #- "traefik.enable=true"
- "traefik.http.middlewares.pihole-admin.addprefix.prefix=/admin" #- "traefik.http.middlewares.pihole-admin.addprefix.prefix=/admin"
- "traefik.http.services.pihole.loadbalancer.server.port=80" #- "traefik.http.services.pihole.loadbalancer.server.port=80"
- "traefik.http.routers.pihole-secure.middlewares=localNetwork@file" #- "traefik.http.routers.pihole-secure.middlewares=localNetwork@file"
- "traefik.http.routers.pihole-secure.entrypoints=web-secure" #- "traefik.http.routers.pihole-secure.entrypoints=web-secure"
- "traefik.http.routers.pihole-secure.rule=Host(`pihole.${DOMAIN}`)" #- "traefik.http.routers.pihole-secure.rule=Host(`pihole.${DOMAIN}`)"
- "traefik.http.routers.pihole-secure.tls.certresolver=milvert_dns" #- "traefik.http.routers.pihole-secure.tls.certresolver=milvert_dns"
- "traefik.http.routers.pihole-secure.tls=true" #- "traefik.http.routers.pihole-secure.tls=true"
- "traefik.http.routers.pihole-secure.middlewares=chain-no-auth@file" #- "traefik.http.routers.pihole-secure.middlewares=chain-no-auth@file"
networks: networks:
frontend: frontend:

View File

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

View File

@ -1,8 +1,13 @@
version: "3.9"
secrets: secrets:
authelia_jwt_secret: authelia_jwt_secret:
file: $SECRETSDIR/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: authelia_session_secret:
file: $SECRETSDIR/authelia_session_secret file: $SECRETSDIR/authelia_session_secret
authelia_storage_postgres_password: authelia_storage_postgres_password:
@ -32,15 +37,13 @@ x-common-keys-monitoring: &common-keys-monitoring
networks: networks:
- backend - backend
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true restart: always
restart: always
services: services:
reverse-proxy: reverse-proxy:
# The official v2.0 Traefik docker image # The official v2.0 Traefik docker image
image: traefik:v2.10 #image: traefik:v2.11
image: traefik:v3.1
container_name: "traefik" container_name: "traefik"
logging: logging:
driver: "json-file" driver: "json-file"
@ -55,6 +58,8 @@ services:
# The HTTP port # The HTTP port
- "80:80" - "80:80"
- "443:443" - "443:443"
# Insecure port
- "8080:8080"
# Influx # Influx
- "8086:8086" - "8086:8086"
# Mqtt # Mqtt
@ -63,23 +68,23 @@ services:
volumes: volumes:
# So that Traefik can listen to the Docker events # So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ${DIR_LOCAL}/traefik/log:/log:rw
- ./traefik.yml:/etc/traefik/traefik.yml - ./traefik.yml:/etc/traefik/traefik.yml
- ./traefik:/rules - ./traefik:/rules
- ./static_config.yml:/etc/traefik/static_config.yml - ./static_config.yml:/etc/traefik/static_config.yml
# - "./log.json:/etc/traefik/log.json" # - "./log.json:/etc/traefik/log.json"
#- ./acme.json:/acme.json # - ./acme.json:/acme.json
- ./letsencrypt/acme.json:/letsencrypt/acme.json - ./letsencrypt/:/letsencrypt:rw
- ${DIR}/traefik/log:/log # - ./letsencrypt/acme_peek_staged.json:/letsencrypt/acme_peek_staged.json
environment: environment:
- CF_API_EMAIL=simon@milvert.com - CF_API_EMAIL=simon@milvert.com
- CF_API_KEY=48d9ae3752afb6e73d99d23c432ba8e38b24c #- CF_DNS_API_TOKEN=48d9ae3752afb6e73d99d23c432ba8e38b24c
- CF_DNS_API_TOKEN=m-X93yWXyvQ2vDhfNLURcQTWOqle13aBbw7g2Zxg
- CLOUDFLARE_IPS - CLOUDFLARE_IPS
- LOCAL_IPS - LOCAL_IPS
labels: labels:
- diun.enable=true - diun.enable=true
- "traefik.http.routers.zighome-secure.middlewares=chain-authelia@file" - "traefik.http.routers.zighome-secure.middlewares=chain-authelia@file"
dns:
- 8.8.8.8
authelia: authelia:
image: authelia/authelia:4 image: authelia/authelia:4
@ -96,12 +101,16 @@ services:
- TZ=$TZ - TZ=$TZ
- PUID=$PUID - PUID=$PUID
- PGID=$PGID - 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_SESSION_SECRET_FILE=/run/secrets/authelia_session_secret
- AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE=/run/secrets/authelia_storage_postgres_password - AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE=/run/secrets/authelia_storage_postgres_password
#- AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/run/secrets/authelia_notifier_smtp_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_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_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: labels:
- diun.enable=true - diun.enable=true
- "traefik.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 - "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email" # yamllint disable-line rule:line-length
secrets: secrets:
- authelia_jwt_secret - authelia_jwt_secret
- authelia_oidc_pem_secret
- authelia_oidc_hamc_secret
- authelia_oidc_key_secret
- authelia_session_secret - authelia_session_secret
- authelia_storage_postgres_password - authelia_storage_postgres_password
- authelia_notifier_smtp_password - authelia_notifier_smtp_password
- authelia_duo_api_secret_key - authelia_duo_api_secret_key
- authelia_storage_encryption_key_file - 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: adguard:
container_name: adguard container_name: adguard
image: adguard/adguardhome:v0.107.40 image: adguard/adguardhome:v0.107.52
restart: unless-stopped restart: unless-stopped
networks: networks:
docker_vlan: docker_vlan:
@ -145,9 +192,9 @@ services:
labels: labels:
- diun.enable=true - diun.enable=true
- "traefik.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.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.middlewares=chain-authelia@file"
- "traefik.http.routers.adguard.tls.certresolver=milvert_dns" - "traefik.http.routers.adguard.tls.certresolver=milvert_dns"
- "traefik.http.routers.adguard.tls=true" - "traefik.http.routers.adguard.tls=true"
@ -171,7 +218,7 @@ services:
networks: networks:
- backend - backend
volumes: volumes:
- ${DIR}/database:/var/lib/mysql:rw - ${DIR_LOCAL}/database:/var/lib/mysql:rw
ports: ports:
- "3307:3306" - "3307:3306"
labels: labels:
@ -199,7 +246,7 @@ services:
labels: labels:
- diun.enable=true - diun.enable=true
- "traefik.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.middlewares=webdb-mid"
- "traefik.http.routers.webdb-secure.entrypoints=web-secure" - "traefik.http.routers.webdb-secure.entrypoints=web-secure"
- "traefik.http.routers.webdb-secure.rule=Host(`webdb.${DOMAIN}`)" - "traefik.http.routers.webdb-secure.rule=Host(`webdb.${DOMAIN}`)"
@ -226,7 +273,7 @@ services:
networks: networks:
- backend - backend
volumes: volumes:
- ${DIR}/database_pg/data:/var/lib/postgresql/data - ${DIR_LOCAL}/database_pg/data:/var/lib/postgresql/data
labels: labels:
- diun.enable=true - diun.enable=true
- "traefik.enable=false" - "traefik.enable=false"
@ -282,7 +329,7 @@ services:
networks: networks:
- backend - backend
volumes: 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 #entrypoint: redis-server --appendonly yes --requirepass $REDIS_PASSWORD --maxmemory 512mb --maxmemory-policy allkeys-lru
labels: labels:
- diun.enable=true - diun.enable=true
@ -295,7 +342,7 @@ services:
ha: ha:
container_name: ha container_name: ha
image: homeassistant/home-assistant:2023.11 image: homeassistant/home-assistant:2025.2
restart: always restart: always
privileged: true privileged: true
networks: networks:
@ -395,11 +442,9 @@ services:
command: command:
- evcc - evcc
container_name: evcc container_name: evcc
image: evcc/evcc:0.123.7 image: evcc/evcc:0.200.5
ports: ports:
- 7070:7070/tcp - 7070:7070/tcp
dns:
- 8.8.8.8
volumes: volumes:
- "./evcc/evcc.yaml:/etc/evcc.yaml:ro" - "./evcc/evcc.yaml:/etc/evcc.yaml:ro"
- ./evcc/evcc:/root/.evcc - ./evcc/evcc:/root/.evcc
@ -417,7 +462,7 @@ services:
- "traefik.http.routers.evcc.tls=true" - "traefik.http.routers.evcc.tls=true"
grafana: grafana:
image: grafana/grafana:10.0.0 image: grafana/grafana:10.3.1
container_name: grafana container_name: grafana
logging: logging:
driver: "json-file" driver: "json-file"
@ -431,7 +476,7 @@ services:
- ./grafana/grafana.ini:/etc/grafana/grafana.ini - ./grafana/grafana.ini:/etc/grafana/grafana.ini
# Data persistency # Data persistency
# sudo mkdir -p /srv/docker/grafana/data; chown 472:472 /srv/docker/grafana/data # 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: labels:
- diun.enable=true - diun.enable=true
- "traefik.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.certresolver=milvert_dns"
- "traefik.http.routers.grafana-secure.tls=true" - "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 ############################
# #
# WEB # WEB
@ -472,8 +660,38 @@ services:
- "traefik.http.routers.librespeed.tls=true" - "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: vwsfriend:
image: tillsteinbach/vwsfriend:0.24.2 image: tillsteinbach/vwsfriend:0.24.7
container_name: vwfriend container_name: vwfriend
ports: ports:
- ${VWSFRIEND_PORT-4000}:${VWSFRIEND_PORT-4000} - ${VWSFRIEND_PORT-4000}:${VWSFRIEND_PORT-4000}
@ -515,7 +733,7 @@ services:
- "traefik.http.routers.vwsfriend.tls=true" - "traefik.http.routers.vwsfriend.tls=true"
vwgrafana: vwgrafana:
image: tillsteinbach/vwsfriend-grafana:0.24.2 image: tillsteinbach/vwsfriend-grafana:0.24.5
container_name: vwgrafana container_name: vwgrafana
ports: ports:
- ${GF_SERVER_HTTP_PORT-3001}:${GF_SERVER_HTTP_PORT-3000} - ${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.certresolver=milvert_dns"
- "traefik.http.routers.vwgrafana.tls=true" - "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: networks:
frontend: frontend:

View File

@ -1,8 +1,8 @@
sponsortoken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODEzODIyNTYsImlhdCI6MTY4Njc3NDI1NiwiaXNzIjoiZXZjYy5pbyIsInN1YiI6Im1pbHZlcnQifQ.HUEmc0NSPt9x5MbOHUAGU6bp3H3E3qwu6O6BAHH9FvE sponsortoken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJldmNjLmlvIiwic3ViIjoibWlsdmVydCIsImV4cCI6MTgzMDE5MzIwMCwiaWF0IjoxNzM1NTg1MjAwLCJzcmMiOiJnaCJ9._K23QsA15DIHRjujwH8rnFZyloSw1RPIeIS4W5WLFGE
log: error log: error
#levels: levels:
# car: trace tariff: trace
interval: 30s interval: 30s
@ -40,14 +40,20 @@ chargers:
password: X7#aEzjlEysBgl password: X7#aEzjlEysBgl
charger: EHCNF485 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: loadpoints:
- title: Garage - title: Garage
charger: wallbox charger: wallbox
vehicle: car vehicle: car
circuit: main
mode: pv mode: pv
phases: 3 phases: 3
mincurrent: 6
maxcurrent: 16
enable: enable:
threshold: 0 threshold: 0
delay: 15s delay: 15s
@ -73,18 +79,19 @@ meters:
energy: energy:
source: mqtt source: mqtt
topic: inverter/calculated/accumulated_yield_energy topic: inverter/calculated/accumulated_yield_energy
# jq: .value timeout: 60s
timeout: 1h
currents: currents:
- source: mqtt - source: mqtt
topic: inverter/measure/phase_A_current topic: inverter/measure/phase_A_current
timeout: 60s
# jq: .value # jq: .value
- source: mqtt - source: mqtt
topic: inverter/measure/phase_B_current topic: inverter/measure/phase_B_current
timeout: 60s
# jq: .value # jq: .value
- source: mqtt - source: mqtt
topic: inverter/measure/phase_C_current topic: inverter/measure/phase_C_current
# jq: .value timeout: 60s
- name: my_grid - name: my_grid
type: custom type: custom
@ -94,41 +101,34 @@ meters:
- source: mqtt - source: mqtt
topic: dsmr/reading/electricity_currently_returned topic: dsmr/reading/electricity_currently_returned
scale: -1000 scale: -1000
timeout: 30s
- source: mqtt - source: mqtt
topic: dsmr/reading/electricity_currently_delivered topic: dsmr/reading/electricity_currently_delivered
scale: 1000 scale: 1000
timeout: 30s
energy: energy:
source: calc source: calc
add: add:
- source: mqtt - source: mqtt
topic: dsmr/reading/electricity_returned_1 topic: dsmr/reading/electricity_returned_1
scale: 0.001 scale: 0.001
timeout: 30s
- source: mqtt - source: mqtt
topic: dsmr/reading/electricity_delivered_1 topic: dsmr/reading/electricity_delivered_1
scale: -0.001 scale: -0.001
timeout: 30s
currents: currents:
- source: calc - source: mqtt
add: topic: dsmr/reading/phase_power_current_l1
- source: mqtt timeout: 30s
topic: dsmr/reading/phase_currently_delivered_l1 - source: mqtt
scale: -1 topic: dsmr/reading/phase_power_current_l2
- source: mqtt timeout: 30s
topic: dsmr/reading/phase_currently_returned_l1 - source: mqtt
- source: calc topic: dsmr/reading/phase_power_current_l1
add: timeout: 30s
- 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
influx: influx:
url: http://influx:8086 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 or disable loading other base map layers
;enable_custom_baselayers = true ;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' - id: '1700687177645'
alias: Handle_motorvärmare alias: Handle_motorvärmare
description: '' description: ''
trigger: triggers:
- platform: time - at: input_datetime.motorvarmare_start
at: input_datetime.motorvarmare_start trigger: time
condition: conditions:
- condition: state - condition: state
entity_id: input_boolean.motorvarmare_toogle entity_id: input_boolean.motorvarmare_toogle
state: 'on' state: 'on'
action: actions:
- service: switch.turn_on - data: {}
data: {} action: switch.turn_on
target: 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 mode: single
- id: '1700693056778' - id: '1700693056778'
description: '' alias: motorvärmare 2h
description: motorvärmare i 2h
trigger: trigger:
- platform: state - platform: state
entity_id: entity_id:
@ -62,7 +68,7 @@
entity_id: switch.nodeid_22_switch entity_id: switch.nodeid_22_switch
mode: single mode: single
- id: '1703971688590' - id: '1703971688590'
alias: Lampa trappa alias: Lampa trappa dag
description: '' description: ''
trigger: trigger:
- platform: state - platform: state
@ -82,6 +88,53 @@
- service: light.turn_on - service: light.turn_on
data: data:
brightness_pct: 52 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: target:
device_id: ad8c90d56d6753ae960fe61560f1de66 device_id: ad8c90d56d6753ae960fe61560f1de66
- delay: - delay:
@ -94,3 +147,470 @@
target: target:
device_id: ad8c90d56d6753ae960fe61560f1de66 device_id: ad8c90d56d6753ae960fe61560f1de66
mode: single 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 # Load frontend themes from the themes folder
frontend: frontend:
themes: !include_dir_merge_named themes themes: !include_dir_merge_named themes
extra_module_url:
- /config/www/community/lovelace-card-mod/card-mod.js
automation: !include automations.yaml automation: !include automations.yaml
script: !include scripts.yaml script: !include scripts.yaml
scene: !include scenes.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: homeassistant:
internal_url: http://10.0.0.203:8123
external_url: https://ha.milvert.com external_url: https://ha.milvert.com
auth_providers: auth_providers:
- type: homeassistant - type: homeassistant
- type: legacy_api_password
api_password: !secret http_password
packages: !include_dir_named packages packages: !include_dir_named packages
#evcc: !include packages/evcc.yaml #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: sonos:
media_player: media_player:
advertise_addr: 10.0.0.203 advertise_addr: 10.0.0.203
@ -29,6 +73,19 @@ sonos:
- 10.0.3.33 - 10.0.3.33
- 10.0.3.32 - 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: http:
use_x_forwarded_for: true use_x_forwarded_for: true
@ -37,19 +94,20 @@ http:
trusted_proxies: trusted_proxies:
- 10.0.0.223 - 10.0.0.223
- 172.19.0.0/24 - 172.19.0.0/24
panel_iframe: template:
configurator: - sensor:
title: Configurator - name: "Vardagsrum Source"
icon: mdi:wrench state: >
url: http://10.0.0.3:3218 {% set source = state_attr('media_player.vardagsrum', 'source') | trim %}
require_admin: true {% if source == "TV" %}
TV
zwave: {% else %}
title: zwave {{ source if source else "none" }}
icon: mdi:wrench {% endif %}
url: http://10.0.0.3:8091 attributes:
require_admin: true 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 For more details about this integration, please refer to the documentation at
https://hacs.xyz/ https://hacs.xyz/
""" """
from __future__ import annotations
import os from __future__ import annotations
from typing import Any
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
from aiogithubapi.const import ACCEPT_HEADERS from aiogithubapi.const import ACCEPT_HEADERS
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from homeassistant.components.frontend import async_remove_panel
from homeassistant.components.lovelace.system_health import system_health_info from homeassistant.components.lovelace.system_health import system_health_info
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, __version__ as HAVERSION from homeassistant.const import Platform, __version__ as HAVERSION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.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.helpers.start import async_at_start
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase 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 .data_client import HacsDataClient
from .enums import ConfigurationType, HacsDisabledReason, HacsStage, LovelaceMode from .enums import HacsDisabledReason, HacsStage, LovelaceMode
from .frontend import async_register_frontend from .frontend import async_register_frontend
from .utils.configuration_schema import hacs_config_combined
from .utils.data import HacsData from .utils.data import HacsData
from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager from .utils.queue_manager import QueueManager
from .utils.version import version_left_higher_or_equal_then_right from .utils.version import version_left_higher_or_equal_then_right
from .websocket import async_register_websocket_commands 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, hass: HomeAssistant,
*, config_entry: ConfigEntry,
config_entry: ConfigEntry | None = None,
config: dict[str, Any] | None = None,
) -> bool: ) -> bool:
"""Initialize the integration""" """Initialize the integration"""
hass.data[DOMAIN] = hacs = HacsBase() hass.data[DOMAIN] = hacs = HacsBase()
hacs.enable_hacs() hacs.enable_hacs()
if config is not None: if config_entry.source == SOURCE_IMPORT:
if DOMAIN not in config: # Import is not supported
return True hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY: return False
return True
hacs.configuration.update_from_dict(
{
"config_type": ConfigurationType.YAML,
**config[DOMAIN],
"config": config[DOMAIN],
}
)
if config_entry is not None: hacs.configuration.update_from_dict(
if config_entry.source == SOURCE_IMPORT: {
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) "config_entry": config_entry,
return False **config_entry.data,
**config_entry.options,
hacs.configuration.update_from_dict( },
{ )
"config_entry": config_entry,
"config_type": ConfigurationType.CONFIG_ENTRY,
**config_entry.data,
**config_entry.options,
}
)
integration = await async_get_integration(hass, DOMAIN) 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 except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
# If this happens, the users YAML is not valid, we assume YAML mode # If this happens, the users YAML is not valid, we assume YAML mode
pass pass
hacs.log.debug("Configuration type: %s", hacs.configuration.config_type)
hacs.core.config_path = hacs.hass.config.path() hacs.core.config_path = hacs.hass.config.path()
if hacs.core.ha_version is None: if hacs.core.ha_version is None:
@ -131,19 +108,18 @@ async def async_initialize_integration(
"""HACS startup tasks.""" """HACS startup tasks."""
hacs.enable_hacs() hacs.enable_hacs()
for location in ( try:
hass.config.path("custom_components/custom_updater.py"), import custom_components.custom_updater
hass.config.path("custom_components/custom_updater/__init__.py"), except ImportError:
): pass
if os.path.exists(location): else:
hacs.log.critical( hacs.log.critical(
"This cannot be used with custom_updater. " "HACS cannot be used with custom_updater. "
"To use this you need to remove custom_updater form %s", "To use HACS you need to remove custom_updater from `custom_components`",
location, )
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False return False
if not version_left_higher_or_equal_then_right( if not version_left_higher_or_equal_then_right(
hacs.core.ha_version.string, hacs.core.ha_version.string,
@ -160,39 +136,23 @@ async def async_initialize_integration(
hacs.disable_hacs(HacsDisabledReason.RESTORE) hacs.disable_hacs(HacsDisabledReason.RESTORE)
return False 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() hacs.set_active_categories()
async_register_websocket_commands(hass) async_register_websocket_commands(hass)
async_register_frontend(hass, hacs) await async_register_frontend(hass, hacs)
if hacs.configuration.config_type == ConfigurationType.YAML: await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
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],
)
hacs.set_stage(HacsStage.SETUP) hacs.set_stage(HacsStage.SETUP)
if hacs.system.disabled: if hacs.system.disabled:
return False return False
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
hacs.set_stage(HacsStage.WAITING) hacs.set_stage(HacsStage.WAITING)
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts") 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 return not hacs.system.disabled
async def async_try_startup(_=None): async def async_try_startup(_=None):
@ -202,10 +162,7 @@ async def async_initialize_integration(
except AIOGitHubAPIException: except AIOGitHubAPIException:
startup_result = False startup_result = False
if not startup_result: if not startup_result:
if ( if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN:
hacs.configuration.config_type == ConfigurationType.YAML
or hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN
):
hacs.log.info("Could not setup HACS, trying again in 15 min") hacs.log.info("Could not setup HACS, trying again in 15 min")
async_call_later(hass, 900, async_try_startup) async_call_later(hass, 900, async_try_startup)
return return
@ -213,37 +170,19 @@ async def async_initialize_integration(
await async_try_startup() 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! # Mischief managed!
return True 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: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI.""" """Set up this integration using UI."""
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) 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] hacs: HacsBase = hass.data[DOMAIN]
return setup_result and not hacs.system.disabled 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 # Clear out pending queue
hacs.queue.clear() hacs.queue.clear()
for task in hacs.recuring_tasks: for task in hacs.recurring_tasks:
# Cancel all pending tasks # Cancel all pending tasks
task() task()
@ -269,15 +208,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
try: try:
if hass.data.get("frontend_panels", {}).get("hacs"): if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel") hacs.log.info("Removing sidepanel")
hass.components.frontend.async_remove_panel("hacs") async_remove_panel(hass, "hacs")
except AttributeError: except AttributeError:
pass pass
platforms = ["sensor"] unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
if hacs.configuration.experimental:
platforms.append("update")
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, platforms)
hacs.set_stage(None) hacs.set_stage(None)
hacs.disable_hacs(HacsDisabledReason.REMOVED) hacs.disable_hacs(HacsDisabledReason.REMOVED)

View File

@ -1,16 +1,17 @@
"""Base HACS class.""" """Base HACS class."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import timedelta from datetime import timedelta
import gzip import gzip
import logging
import math import math
import os import os
import pathlib import pathlib
import shutil import shutil
from typing import TYPE_CHECKING, Any, Awaitable, Callable from typing import TYPE_CHECKING, Any
from aiogithubapi import ( from aiogithubapi import (
AIOGitHubAPIException, AIOGitHubAPIException,
@ -24,23 +25,22 @@ from aiogithubapi import (
from aiogithubapi.objects.repository import AIOGitHubAPIRepository from aiogithubapi.objects.repository import AIOGitHubAPIRepository
from aiohttp.client import ClientSession, ClientTimeout from aiohttp.client import ClientSession, ClientTimeout
from awesomeversion import AwesomeVersion 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.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send 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.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.loader import Integration from homeassistant.loader import Integration
from homeassistant.util import dt 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 .const import DOMAIN, TV, URL_BASE
from .coordinator import HacsUpdateCoordinator
from .data_client import HacsDataClient from .data_client import HacsDataClient
from .enums import ( from .enums import (
ConfigurationType,
HacsCategory, HacsCategory,
HacsDisabledReason, HacsDisabledReason,
HacsDispatchEvent, HacsDispatchEvent,
@ -58,12 +58,14 @@ from .exceptions import (
HacsRepositoryExistException, HacsRepositoryExistException,
HomeAssistantCoreRepositoryException, HomeAssistantCoreRepositoryException,
) )
from .repositories import RERPOSITORY_CLASSES from .repositories import REPOSITORY_CLASSES
from .utils.decode import decode_content 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.json import json_loads
from .utils.logger import LOGGER from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager from .utils.queue_manager import QueueManager
from .utils.store import async_load_from_store, async_save_to_store from .utils.store import async_load_from_store, async_save_to_store
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING: if TYPE_CHECKING:
from .repositories.base import HacsRepository from .repositories.base import HacsRepository
@ -113,15 +115,11 @@ class HacsConfiguration:
appdaemon: bool = False appdaemon: bool = False
config: dict[str, Any] = field(default_factory=dict) config: dict[str, Any] = field(default_factory=dict)
config_entry: ConfigEntry | None = None config_entry: ConfigEntry | None = None
config_type: ConfigurationType | None = None
country: str = "ALL" country: str = "ALL"
debug: bool = False debug: bool = False
dev: bool = False dev: bool = False
experimental: bool = False
frontend_repo_url: str = "" frontend_repo_url: str = ""
frontend_repo: str = "" frontend_repo: str = ""
netdaemon_path: str = "netdaemon/apps/"
netdaemon: bool = False
plugin_path: str = "www/community/" plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/" python_script_path: str = "python_scripts/"
python_script: bool = False python_script: bool = False
@ -142,6 +140,8 @@ class HacsConfiguration:
raise HacsException("Configuration is not valid.") raise HacsException("Configuration is not valid.")
for key in data: for key in data:
if key in {"experimental", "netdaemon", "release_limit", "debug"}:
continue
self.__setattr__(key, data[key]) self.__setattr__(key, data[key])
@ -217,6 +217,13 @@ class HacsRepositories:
"""Return a list of downloaded repositories.""" """Return a list of downloaded repositories."""
return [repo for repo in self._repositories if repo.data.installed] 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: def register(self, repository: HacsRepository, default: bool = False) -> None:
"""Register a repository.""" """Register a repository."""
repo_id = str(repository.data.id) repo_id = str(repository.data.id)
@ -348,9 +355,6 @@ class HacsRepositories:
class HacsBase: class HacsBase:
"""Base HACS class.""" """Base HACS class."""
common = HacsCommon()
configuration = HacsConfiguration()
core = HacsCore()
data: HacsData | None = None data: HacsData | None = None
data_client: HacsDataClient | None = None data_client: HacsDataClient | None = None
frontend_version: str | None = None frontend_version: str | None = None
@ -358,17 +362,24 @@ class HacsBase:
githubapi: GitHubAPI | None = None githubapi: GitHubAPI | None = None
hass: HomeAssistant | None = None hass: HomeAssistant | None = None
integration: Integration | None = None integration: Integration | None = None
log: logging.Logger = LOGGER
queue: QueueManager | None = None queue: QueueManager | None = None
recuring_tasks = []
repositories: HacsRepositories = HacsRepositories()
repository: AIOGitHubAPIRepository | None = None repository: AIOGitHubAPIRepository | None = None
session: ClientSession | None = None session: ClientSession | None = None
stage: HacsStage | None = None stage: HacsStage | None = None
status = HacsStatus()
system = HacsSystem()
validation: ValidationManager | None = None 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 @property
def integration_dir(self) -> pathlib.Path: def integration_dir(self) -> pathlib.Path:
@ -394,12 +405,7 @@ class HacsBase:
if reason != HacsDisabledReason.REMOVED: if reason != HacsDisabledReason.REMOVED:
self.log.error("HACS is disabled - %s", reason) self.log.error("HACS is disabled - %s", reason)
if ( if reason == HacsDisabledReason.INVALID_TOKEN:
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"
self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass) self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass)
def enable_hacs(self) -> None: def enable_hacs(self) -> None:
@ -413,12 +419,14 @@ class HacsBase:
if category not in self.common.categories: if category not in self.common.categories:
self.log.info("Enable category: %s", category) self.log.info("Enable category: %s", category)
self.common.categories.add(category) self.common.categories.add(category)
self.coordinators[category] = HacsUpdateCoordinator()
def disable_hacs_category(self, category: HacsCategory) -> None: def disable_hacs_category(self, category: HacsCategory) -> None:
"""Disable HACS category.""" """Disable HACS category."""
if category in self.common.categories: if category in self.common.categories:
self.log.info("Disabling category: %s", category) self.log.info("Disabling category: %s", category)
self.common.categories.pop(category) self.common.categories.pop(category)
self.coordinators.pop(category)
async def async_save_file(self, file_path: str, content: Any) -> bool: async def async_save_file(self, file_path: str, content: Any) -> bool:
"""Save a file.""" """Save a file."""
@ -451,12 +459,13 @@ class HacsBase:
try: try:
await self.hass.async_add_executor_job(_write_file) await self.hass.async_add_executor_job(_write_file)
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as error: ) as error:
self.log.error("Could not write data to %s - %s", file_path, error) self.log.error("Could not write data to %s - %s", file_path, error)
return False return False
return os.path.exists(file_path) return await async_exists(self.hass, file_path)
async def async_can_update(self) -> int: async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for.""" """Helper to calculate the number of repositories we can fetch data for."""
@ -472,24 +481,13 @@ class HacsBase:
) )
self.disable_hacs(HacsDisabledReason.RATE_LIMIT) self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.log.exception(exception) self.log.exception(exception)
return 0 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( async def async_github_api_method(
self, self,
method: Callable[[], Awaitable[TV]], method: Callable[[], Awaitable[TV]],
@ -513,7 +511,8 @@ class HacsBase:
except GitHubException as exception: except GitHubException as exception:
_exception = exception _exception = exception
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.log.exception(exception) self.log.exception(exception)
_exception = exception _exception = exception
@ -545,7 +544,7 @@ class HacsBase:
): ):
raise AddonRepositoryException() raise AddonRepositoryException()
if category not in RERPOSITORY_CLASSES: if category not in REPOSITORY_CLASSES:
self.log.warning( self.log.warning(
"%s is not a valid repository category, %s will not be registered.", "%s is not a valid repository category, %s will not be registered.",
category, category,
@ -556,7 +555,7 @@ class HacsBase:
if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None: if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None:
repository_full_name = renamed repository_full_name = renamed
repository: HacsRepository = RERPOSITORY_CLASSES[category](self, repository_full_name) repository: HacsRepository = REPOSITORY_CLASSES[category](self, repository_full_name)
if check: if check:
try: try:
await repository.async_registration(ref) await repository.async_registration(ref)
@ -566,7 +565,8 @@ class HacsBase:
self.log.error("Validation for %s failed.", repository_full_name) self.log.error("Validation for %s failed.", repository_full_name)
if self.system.action: if self.system.action:
raise HacsException( raise HacsException(
f"::error:: Validation for {repository_full_name} failed." f"::error:: Validation for {
repository_full_name} failed."
) )
return repository.validate.errors return repository.validate.errors
if self.system.action: if self.system.action:
@ -582,7 +582,8 @@ class HacsBase:
except AIOGitHubAPIException as exception: except AIOGitHubAPIException as exception:
self.common.skip.add(repository.data.full_name) self.common.skip.add(repository.data.full_name)
raise HacsException( raise HacsException(
f"Validation for {repository_full_name} failed with {exception}." f"Validation for {
repository_full_name} failed with {exception}."
) from exception ) from exception
if self.status.new: if self.status.new:
@ -592,7 +593,7 @@ class HacsBase:
repository.data.id = repository_id repository.data.id = repository_id
else: 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( self.async_dispatch(
HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.REPOSITORY,
{ {
@ -613,91 +614,84 @@ class HacsBase:
for repo in critical: for repo in critical:
if not repo["acknowledged"]: if not repo["acknowledged"]:
self.log.critical("URGENT!: Check the HACS panel!") self.log.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create( async_create_persistent_notification(
title="URGENT!", message="**Check the HACS panel!**" self.hass, title="URGENT!", message="**Check the HACS panel!**"
) )
break break
if not self.configuration.experimental: self.recurring_tasks.append(
self.recuring_tasks.append( async_track_time_interval(
self.hass.helpers.event.async_track_time_interval( self.hass,
self.async_update_downloaded_repositories, timedelta(hours=48) self.async_load_hacs_from_github,
) 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.recuring_tasks.append( self.recurring_tasks.append(
self.hass.helpers.event.async_track_time_interval( async_track_time_interval(
self.async_get_all_category_repositories, timedelta(hours=6) self.hass, self.async_update_downloaded_custom_repositories, timedelta(hours=48)
) )
) )
self.recuring_tasks.append( self.recurring_tasks.append(
self.hass.helpers.event.async_track_time_interval( async_track_time_interval(
self.async_check_rate_limit, timedelta(minutes=5) 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_prosess_queue, timedelta(minutes=10)
) )
) )
self.recuring_tasks.append( self.recurring_tasks.append(
self.hass.helpers.event.async_track_time_interval( async_track_time_interval(self.hass, self.async_check_rate_limit, timedelta(minutes=5))
self.async_handle_critical_repositories, timedelta(hours=6) )
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 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.status.startup = False
self.async_dispatch(HacsDispatchEvent.STATUS, {}) self.async_dispatch(HacsDispatchEvent.STATUS, {})
await self.async_handle_removed_repositories() await self.async_handle_removed_repositories()
await self.async_get_all_category_repositories() await self.async_get_all_category_repositories()
await self.async_update_downloaded_repositories()
self.set_stage(HacsStage.RUNNING) self.set_stage(HacsStage.RUNNING)
self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True}) self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
await self.async_handle_critical_repositories() await self.async_handle_critical_repositories()
await self.async_prosess_queue() await self.async_process_queue()
self.async_dispatch(HacsDispatchEvent.STATUS, {}) 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.""" """Download files, and return the content."""
if url is None: if url is None:
return None return None
if "tags/" in url: if not keep_url and "tags/" in url:
url = url.replace("tags/", "") url = url.replace("tags/", "")
self.log.debug("Downloading %s", url) self.log.debug("Trying to download %s", url)
timeouts = 0 timeouts = 0
while timeouts < 5: while timeouts < 5:
@ -713,9 +707,10 @@ class HacsBase:
return await request.read() return await request.read()
raise HacsException( 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( self.log.warning(
"A timeout of 60! seconds was encountered while downloading %s, " "A timeout of 60! seconds was encountered while downloading %s, "
"using over 60 seconds to download a single file is not normal. " "using over 60 seconds to download a single file is not normal. "
@ -731,23 +726,34 @@ class HacsBase:
continue continue
except ( except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except # lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception: ) as exception:
self.log.exception("Download failed - %s", exception) if not nolog:
self.log.exception("Download failed - %s", exception)
return None return None
async def async_recreate_entities(self) -> None: async def async_recreate_entities(self) -> None:
"""Recreate entities.""" """Recreate entities."""
if self.configuration == ConfigurationType.YAML or not self.configuration.experimental: platforms = [Platform.UPDATE]
return
platforms = [Platform.SENSOR, Platform.UPDATE] # Workaround for core versions without https://github.com/home-assistant/core/pull/117084
if self.core.ha_version < AwesomeVersion("2024.6.0"):
await self.hass.config_entries.async_unload_platforms( unload_platforms_lock = asyncio.Lock()
entry=self.configuration.config_entry, async with unload_platforms_lock:
platforms=platforms, 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( await self.hass.config_entries.async_forward_entry_setups(
self.configuration.config_entry, platforms self.configuration.config_entry, platforms
) )
@ -760,49 +766,40 @@ class HacsBase:
def set_active_categories(self) -> None: def set_active_categories(self) -> None:
"""Set the active categories.""" """Set the active categories."""
self.common.categories = set() 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)) self.enable_hacs_category(HacsCategory(category))
if self.configuration.experimental and self.core.ha_version >= "2023.4.0b0": if (
self.enable_hacs_category(HacsCategory.TEMPLATE) HacsCategory.PYTHON_SCRIPT in self.hass.config.components
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
if HacsCategory.PYTHON_SCRIPT in self.hass.config.components: ):
self.enable_hacs_category(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) self.enable_hacs_category(HacsCategory.THEME)
if self.configuration.appdaemon: if self.configuration.appdaemon:
self.enable_hacs_category(HacsCategory.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: async def async_load_hacs_from_github(self, _=None) -> None:
"""Load HACS from GitHub.""" """Load HACS from GitHub."""
if self.configuration.experimental and self.status.inital_fetch_done: if self.status.inital_fetch_done:
return return
try: try:
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION) repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
should_recreate_entities = False
if repository is None: if repository is None:
should_recreate_entities = True
await self.async_register_repository( await self.async_register_repository(
repository_full_name=HacsGitHubRepo.INTEGRATION, repository_full_name=HacsGitHubRepo.INTEGRATION,
category=HacsCategory.INTEGRATION, category=HacsCategory.INTEGRATION,
default=True, default=True,
) )
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION) 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.log.error("Scheduling update of hacs/integration")
self.queue.add(repository.common_update()) self.queue.add(repository.common_update())
if repository is None: if repository is None:
@ -813,6 +810,9 @@ class HacsBase:
repository.data.new = False repository.data.new = False
repository.data.releases = True repository.data.releases = True
if should_recreate_entities:
await self.async_recreate_entities()
self.repository = repository.repository_object self.repository = repository.repository_object
self.repositories.mark_default(repository) self.repositories.mark_default(repository)
except HacsException as exception: except HacsException as exception:
@ -832,8 +832,6 @@ class HacsBase:
await asyncio.gather( await asyncio.gather(
*[ *[
self.async_get_category_repositories_experimental(category) 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 [] for category in self.common.categories or []
] ]
) )
@ -842,7 +840,7 @@ class HacsBase:
"""Update all category repositories.""" """Update all category repositories."""
self.log.debug("Fetching updated content for %s", category) self.log.debug("Fetching updated content for %s", category)
try: try:
category_data = await self.data_client.get_data(category) category_data = await self.data_client.get_data(category, validate=True)
except HacsNotModifiedException: except HacsNotModifiedException:
self.log.debug("No updates for %s", category) self.log.debug("No updates for %s", category)
return return
@ -853,14 +851,14 @@ class HacsBase:
await self.data.register_unknown_repositories(category_data, category) await self.data.register_unknown_repositories(category_data, category)
for repo_id, repo_data in category_data.items(): for repo_id, repo_data in category_data.items():
repo = repo_data["full_name"] repo_name = repo_data["full_name"]
if self.common.renamed_repositories.get(repo): if self.common.renamed_repositories.get(repo_name):
repo = self.common.renamed_repositories[repo] repo_name = self.common.renamed_repositories[repo_name]
if self.repositories.is_removed(repo): if self.repositories.is_removed(repo_name):
continue continue
if repo in self.common.archived_repositories: if repo_name in self.common.archived_repositories:
continue 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.set_repository_id(repository, repo_id)
self.repositories.mark_default(repository) self.repositories.mark_default(repository)
if repository.data.last_fetched is None or ( if repository.data.last_fetched is None or (
@ -871,15 +869,6 @@ class HacsBase:
repository.repository_manifest.update_data( repository.repository_manifest.update_data(
{**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest} {**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": if category == "integration":
self.status.inital_fetch_done = True self.status.inital_fetch_done = True
@ -896,50 +885,8 @@ class HacsBase:
) )
self.repositories.unregister(repository) self.repositories.unregister(repository)
async def async_get_category_repositories(self, category: HacsCategory) -> None: self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
"""Get repositories from category.""" self.coordinators[category].async_update_listeners()
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")
async def async_check_rate_limit(self, _=None) -> None: async def async_check_rate_limit(self, _=None) -> None:
"""Check rate limit.""" """Check rate limit."""
@ -951,9 +898,9 @@ class HacsBase:
self.log.debug("Ratelimit indicate we can update %s", can_update) self.log.debug("Ratelimit indicate we can update %s", can_update)
if can_update > 0: if can_update > 0:
self.enable_hacs() 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.""" """Process the queue."""
if self.system.disabled: if self.system.disabled:
self.log.debug("HACS is disabled") self.log.debug("HACS is disabled")
@ -993,12 +940,7 @@ class HacsBase:
self.log.info("Loading removed repositories") self.log.info("Loading removed repositories")
try: try:
if self.configuration.experimental: removed_repositories = await self.data_client.get_data("removed", validate=True)
removed_repositories = await self.data_client.get_data("removed")
else:
removed_repositories = await self.async_github_get_hacs_default_file(
HacsCategory.REMOVED
)
except HacsException: except HacsException:
return return
@ -1013,21 +955,20 @@ class HacsBase:
continue continue
if repository.data.installed: if repository.data.installed:
if removed.removal_type != "critical": if removed.removal_type != "critical":
if self.configuration.experimental: async_create_issue(
async_create_issue( hass=self.hass,
hass=self.hass, domain=DOMAIN,
domain=DOMAIN, issue_id=f"removed_{repository.data.id}",
issue_id=f"removed_{repository.data.id}", is_fixable=False,
is_fixable=False, issue_domain=DOMAIN,
issue_domain=DOMAIN, severity=IssueSeverity.WARNING,
severity=IssueSeverity.WARNING, translation_key="removed",
translation_key="removed", translation_placeholders={
translation_placeholders={ "name": repository.data.full_name,
"name": repository.data.full_name, "reason": removed.reason,
"reason": removed.reason, "repositry_id": repository.data.id,
"repositry_id": repository.data.id, },
}, )
)
self.log.warning( self.log.warning(
"You have '%s' installed with HACS " "You have '%s' installed with HACS "
"this repository has been removed from HACS, please consider removing it. " "this repository has been removed from HACS, please consider removing it. "
@ -1042,30 +983,43 @@ class HacsBase:
if need_to_save: if need_to_save:
await self.data.async_write() 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: async def async_update_downloaded_custom_repositories(self, _=None) -> None:
"""Execute the task.""" """Execute the task."""
if self.system.disabled or not self.configuration.experimental: if self.system.disabled:
return return
self.log.info("Starting recurring background task for downloaded custom repositories") 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: for repository in self.repositories.list_downloaded:
if ( if (
repository.data.category in self.common.categories repository.data.category in self.common.categories
and not self.repositories.is_default(repository.data.id) 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") self.log.debug("Recurring background task for downloaded custom repositories done")
@ -1077,10 +1031,7 @@ class HacsBase:
was_installed = False was_installed = False
try: try:
if self.configuration.experimental: critical = await self.data_client.get_data("critical", validate=True)
critical = await self.data_client.get_data("critical")
else:
critical = await self.async_github_get_hacs_default_file("critical")
except (GitHubNotModifiedException, HacsNotModifiedException): except (GitHubNotModifiedException, HacsNotModifiedException):
return return
except HacsException: except HacsException:
@ -1134,11 +1085,10 @@ class HacsBase:
self.log.critical("Restarting Home Assistant") self.log.critical("Restarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100)) self.hass.async_create_task(self.hass.async_stop(100))
@callback async def async_setup_frontend_endpoint_plugin(self) -> None:
def async_setup_frontend_endpoint_plugin(self) -> None:
"""Setup the http endpoints for plugins if its not already handled.""" """Setup the http endpoints for plugins if its not already handled."""
if self.status.active_frontend_endpoint_plugin or not os.path.exists( if self.status.active_frontend_endpoint_plugin or not await async_exists(
self.hass.config.path("www/community") self.hass, self.hass.config.path("www/community")
): ):
return return
@ -1150,26 +1100,11 @@ class HacsBase:
use_cache, use_cache,
) )
self.hass.http.register_static_path( await async_register_static_path(
self.hass,
URL_BASE, URL_BASE,
self.hass.config.path("www/community"), self.hass.config.path("www/community"),
cache_headers=use_cache, cache_headers=use_cache,
) )
self.status.active_frontend_endpoint_plugin = True 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.""" """Adds config flow for HACS."""
from __future__ import annotations from __future__ import annotations
import asyncio
from contextlib import suppress
from typing import TYPE_CHECKING 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 aiogithubapi.common.const import OAUTH_USER_LOGIN
from awesomeversion import AwesomeVersion 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.const import __version__ as HAVERSION
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import UnknownFlow
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_call_later
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
import voluptuous as vol import voluptuous as vol
from .base import HacsBase from .base import HacsBase
from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION
from .enums import ConfigurationType
from .utils.configuration_schema import ( from .utils.configuration_schema import (
APPDAEMON, APPDAEMON,
COUNTRY, COUNTRY,
DEBUG,
EXPERIMENTAL,
NETDAEMON,
RELEASE_LIMIT,
SIDEPANEL_ICON, SIDEPANEL_ICON,
SIDEPANEL_TITLE, SIDEPANEL_TITLE,
) )
@ -33,23 +36,22 @@ if TYPE_CHECKING:
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for HACS.""" """Config flow for HACS."""
hass: HomeAssistant
VERSION = 1 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.""" """Initialize."""
self._errors = {} 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 = {} self._user_input = {}
async def async_step_user(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) return await self.async_step_device(user_input)
## Initial form # Initial form
return await self._show_config_form(user_input) return await self._show_config_form(user_input)
async def async_step_device(self, _user_input): async def async_step_device(self, _user_input):
"""Handle device steps""" """Handle device steps."""
async def _wait_for_activation(_=None): async def _wait_for_activation() -> None:
if self._login_device is None or self._login_device.expires_in is None: try:
async_call_later(self.hass, 1, _wait_for_activation) response = await self.device.activation(device_code=self._registration.device_code)
return self._activation = response.data
finally:
response = await self.device.activation(device_code=self._login_device.device_code) async def _progress():
self.activation = response.data with suppress(UnknownFlow):
self.hass.async_create_task( await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
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) integration = await async_get_integration(self.hass, DOMAIN)
if not self.device: self.device = GitHubDeviceAPI(
self.device = GitHubDeviceAPI( client_id=CLIENT_ID,
client_id=CLIENT_ID, session=aiohttp_client.async_get_clientsession(self.hass),
session=aiohttp_client.async_get_clientsession(self.hass), **{"client_name": f"HACS/{integration.version}"},
**{"client_name": f"HACS/{integration.version}"}, )
)
async_call_later(self.hass, 1, _wait_for_activation)
try: try:
response = await self.device.register() response = await self.device.register()
self._login_device = response.data self._registration = 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,
},
)
except GitHubException as exception: except GitHubException as exception:
self.log.error(exception) LOGGER.exception(exception)
return self.async_abort(reason="github") 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): async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data.""" """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) "acc_untested", default=user_input.get("acc_untested", False)
): bool, ): bool,
vol.Required("acc_disable", default=user_input.get("acc_disable", 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, errors=self._errors,
@ -146,7 +152,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if self._reauth: if self._reauth:
existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
self.hass.config_entries.async_update_entry( 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) await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
@ -154,13 +160,17 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry( return self.async_create_entry(
title="", title="",
data={ data={
"token": self.activation.access_token, "token": self._activation.access_token,
}, },
options={ 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): async def async_step_reauth(self, _user_input=None):
"""Perform reauth upon an API authentication error.""" """Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
@ -181,12 +191,13 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return HacsOptionsFlowHandler(config_entry) return HacsOptionsFlowHandler(config_entry)
class HacsOptionsFlowHandler(config_entries.OptionsFlow): class HacsOptionsFlowHandler(OptionsFlow):
"""HACS config flow options handler.""" """HACS config flow options handler."""
def __init__(self, config_entry): def __init__(self, config_entry):
"""Initialize HACS options flow.""" """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): async def async_step_init(self, _user_input=None):
"""Manage the options.""" """Manage the options."""
@ -196,10 +207,7 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
hacs: HacsBase = self.hass.data.get(DOMAIN) hacs: HacsBase = self.hass.data.get(DOMAIN)
if user_input is not None: if user_input is not None:
limit = int(user_input.get(RELEASE_LIMIT, 5)) return self.async_create_entry(title="", data={**user_input, "experimental": True})
if limit <= 0 or limit > 100:
return self.async_abort(reason="release_limit_value")
return self.async_create_entry(title="", data=user_input)
if hacs is None or hacs.configuration is None: if hacs is None or hacs.configuration is None:
return self.async_abort(reason="not_setup") return self.async_abort(reason="not_setup")
@ -207,18 +215,11 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
if hacs.queue.has_pending_tasks: if hacs.queue.has_pending_tasks:
return self.async_abort(reason="pending_tasks") return self.async_abort(reason="pending_tasks")
if hacs.configuration.config_type == ConfigurationType.YAML: schema = {
schema = {vol.Optional("not_in_use", default=""): str} vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
else: vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
schema = { vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str, vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
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,
}
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))

View File

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

View File

@ -1,12 +1,25 @@
"""HACS Data client.""" """HACS Data client."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import Any from typing import Any
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
import voluptuous as vol
from .exceptions import HacsException, HacsNotModifiedException 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: class HacsDataClient:
@ -39,7 +52,7 @@ class HacsDataClient:
response.raise_for_status() response.raise_for_status()
except HacsNotModifiedException: except HacsNotModifiedException:
raise raise
except asyncio.TimeoutError: except TimeoutError:
raise HacsException("Timeout of 60s reached") from None raise HacsException("Timeout of 60s reached") from None
except Exception as exception: except Exception as exception:
raise HacsException(f"Error fetching data from HACS: {exception}") from exception raise HacsException(f"Error fetching data from HACS: {exception}") from exception
@ -48,9 +61,37 @@ class HacsDataClient:
return await response.json() 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.""" """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]: async def get_repositories(self, section: str) -> list[str]:
"""Get repositories.""" """Get repositories."""

View File

@ -1,4 +1,5 @@
"""Diagnostics support for HACS.""" """Diagnostics support for HACS."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@ -10,7 +11,6 @@ from homeassistant.core import HomeAssistant
from .base import HacsBase from .base import HacsBase
from .const import DOMAIN from .const import DOMAIN
from .utils.configuration_schema import TOKEN
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
@ -48,8 +48,6 @@ async def async_get_config_entry_diagnostics(
"country", "country",
"debug", "debug",
"dev", "dev",
"experimental",
"netdaemon",
"python_script", "python_script",
"release_limit", "release_limit",
"theme", "theme",
@ -79,4 +77,4 @@ async def async_get_config_entry_diagnostics(
except GitHubException as exception: except GitHubException as exception:
data["rate_limit"] = str(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.""" """HACS Base entities."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any 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.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity
from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
from .coordinator import HacsUpdateCoordinator
from .enums import HacsDispatchEvent, HacsGitHubRepo from .enums import HacsDispatchEvent, HacsGitHubRepo
if TYPE_CHECKING: if TYPE_CHECKING:
@ -39,6 +42,10 @@ class HacsBaseEntity(Entity):
"""Initialize.""" """Initialize."""
self.hacs = hacs self.hacs = hacs
class HacsDispatcherEntity(HacsBaseEntity):
"""Base HACS entity listening to dispatcher signals."""
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register for status events.""" """Register for status events."""
self.async_on_remove( self.async_on_remove(
@ -64,7 +71,7 @@ class HacsBaseEntity(Entity):
self.async_write_ha_state() self.async_write_ha_state()
class HacsSystemEntity(HacsBaseEntity): class HacsSystemEntity(HacsDispatcherEntity):
"""Base system entity.""" """Base system entity."""
_attr_icon = "hacs:hacs" _attr_icon = "hacs:hacs"
@ -76,7 +83,7 @@ class HacsSystemEntity(HacsBaseEntity):
return system_info(self.hacs) return system_info(self.hacs)
class HacsRepositoryEntity(HacsBaseEntity): class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity):
"""Base repository entity.""" """Base repository entity."""
def __init__( def __init__(
@ -85,9 +92,11 @@ class HacsRepositoryEntity(HacsBaseEntity):
repository: HacsRepository, repository: HacsRepository,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(hacs=hacs) BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category])
HacsBaseEntity.__init__(self, hacs=hacs)
self.repository = repository self.repository = repository
self._attr_unique_id = str(repository.data.id) self._attr_unique_id = str(repository.data.id)
self._repo_last_fetched = repository.data.last_fetched
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -100,20 +109,35 @@ class HacsRepositoryEntity(HacsBaseEntity):
if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION: if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION:
return system_info(self.hacs) 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 { return {
"identifiers": {(DOMAIN, str(self.repository.data.id))}, "identifiers": {(DOMAIN, str(self.repository.data.id))},
"name": self.repository.display_name, "name": self.repository.display_name,
"model": self.repository.data.category, "model": self.repository.data.category,
"manufacturer": ", ".join( "manufacturer": _manufacturer(),
author.replace("@", "") for author in self.repository.data.authors "configuration_url": f"homeassistant://hacs/repository/{self.repository.data.id}",
),
"configuration_url": "homeassistant://hacs",
"entry_type": DeviceEntryType.SERVICE, "entry_type": DeviceEntryType.SERVICE,
} }
@callback @callback
def _update_and_write_state(self, data: dict) -> None: def _handle_coordinator_update(self) -> None:
"""Update the entity and write state.""" """Handle updated data from the coordinator."""
if data.get("repository_id") == self.repository.data.id: if (
self._update() self._repo_last_fetched is not None
self.async_write_ha_state() 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.""" """Helper constants."""
# pylint: disable=missing-class-docstring # pylint: disable=missing-class-docstring
import sys from enum import StrEnum
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
class HacsGitHubRepo(StrEnum): class HacsGitHubRepo(StrEnum):
@ -29,7 +16,6 @@ class HacsCategory(StrEnum):
INTEGRATION = "integration" INTEGRATION = "integration"
LOVELACE = "lovelace" LOVELACE = "lovelace"
PLUGIN = "plugin" # Kept for legacy purposes PLUGIN = "plugin" # Kept for legacy purposes
NETDAEMON = "netdaemon"
PYTHON_SCRIPT = "python_script" PYTHON_SCRIPT = "python_script"
TEMPLATE = "template" TEMPLATE = "template"
THEME = "theme" THEME = "theme"
@ -59,11 +45,6 @@ class RepositoryFile(StrEnum):
MAINIFEST_JSON = "manifest.json" MAINIFEST_JSON = "manifest.json"
class ConfigurationType(StrEnum):
YAML = "yaml"
CONFIG_ENTRY = "config_entry"
class LovelaceMode(StrEnum): class LovelaceMode(StrEnum):
"""Lovelace Modes.""" """Lovelace Modes."""

View File

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