Home Assistant Git Exporter

This commit is contained in:
root
2024-08-26 13:38:09 +02:00
parent 80fc630f5e
commit fc0376e38e
2010 changed files with 11414 additions and 16153 deletions
+4
View File
@@ -18,6 +18,10 @@ HA Add-ons by alexbelgium:
maintainer: alexbelgium maintainer: alexbelgium
slug: db21ed7f slug: db21ed7f
source: https://github.com/alexbelgium/hassio-addons source: https://github.com/alexbelgium/hassio-addons
HACS Add-ons Repository:
maintainer: HACS <hi@hacs.xyz>
slug: cb646a50
source: https://github.com/hacs/addons
'Home Assistant Add-on: Rxcom2MQTT': 'Home Assistant Add-on: Rxcom2MQTT':
maintainer: sguernion maintainer: sguernion
slug: 3bff5a27 slug: 3bff5a27
+1 -1
View File
@@ -1 +1 @@
2024.8.0 2024.8.3
+187 -22
View File
@@ -22,12 +22,12 @@
{ {
"name": "Dates", "name": "Dates",
"id": "5b702fc3a47244c7bfce29e27eb6b19f", "id": "5b702fc3a47244c7bfce29e27eb6b19f",
"complete": false "complete": true
}, },
{ {
"name": "Bananes", "name": "Bananes",
"id": "6aa090cb21b141f8a2b37386e5896711", "id": "6aa090cb21b141f8a2b37386e5896711",
"complete": false "complete": true
}, },
{ {
"name": "Abricot", "name": "Abricot",
@@ -104,26 +104,11 @@
"id": "78dfd45120b04d798ac63020635b096c", "id": "78dfd45120b04d798ac63020635b096c",
"complete": true "complete": true
}, },
{
"name": "Bleu",
"id": "6124a137218348009e641c02c7d36d9d",
"complete": true
},
{
"name": "Blanc",
"id": "2e603522f7dd4d50a31af1acd5986c89",
"complete": true
},
{ {
"name": "Vinaigre blanc", "name": "Vinaigre blanc",
"id": "a4f1a2acde9e4cf29804036b353d8a45", "id": "a4f1a2acde9e4cf29804036b353d8a45",
"complete": true "complete": true
}, },
{
"name": "Grain jaune mouche",
"id": "164ff5e89df14b13bc1e9fc4eccbf859",
"complete": true
},
{ {
"name": "eponge", "name": "eponge",
"id": "7bdaf185b84d408ca1bc510e3f184c8b", "id": "7bdaf185b84d408ca1bc510e3f184c8b",
@@ -174,11 +159,6 @@
"id": "6b3cff6926c14915b9bb1496c2d49a3e", "id": "6b3cff6926c14915b9bb1496c2d49a3e",
"complete": true "complete": true
}, },
{
"name": "Aliment poule",
"id": "941e3e0206e04e34924ec1a0bde9a46b",
"complete": true
},
{ {
"name": "Cassis", "name": "Cassis",
"id": "71f61483307245e9a05c4085daeff9ab", "id": "71f61483307245e9a05c4085daeff9ab",
@@ -217,6 +197,191 @@
{ {
"name": "Tomates grappes", "name": "Tomates grappes",
"id": "5f4420e6430a456db1180ba262a26bd8", "id": "5f4420e6430a456db1180ba262a26bd8",
"complete": true
},
{
"name": "Merguez",
"id": "22cd65ebe10b466ba71cad6072813326",
"complete": true
},
{
"name": "chipolatta",
"id": "5f58104f006046abb7c83f44a2034b5c",
"complete": true
},
{
"name": "poisson",
"id": "6e932be566a7484da37de61d904fdeb0",
"complete": true
},
{
"name": "Saumon",
"id": "3274269326084327a92e9223337a2ee9",
"complete": true
},
{
"name": "Filet truite",
"id": "9907427bd67c49c78c28d5ff9fc32614",
"complete": true
},
{
"name": "Sac poubelle 50l",
"id": "3f56c61e1f214558bee19f97893485c9",
"complete": true
},
{
"name": "Tomate",
"id": "0baf9892ccb24054ae761adf09472418",
"complete": true
},
{
"name": "Benco",
"id": "161d7c04a6ff439e879ebe2468f8cce1",
"complete": true
},
{
"name": "Verrou porte",
"id": "88799c6ef8a441ca96370abd3e4541f6",
"complete": true
},
{
"name": "Prune",
"id": "42eca1bcc1ca4b1c87edbb7d10ee194c",
"complete": true
},
{
"name": "Bruyere",
"id": "fc2834f7408642f39504fe84e77297ab",
"complete": true
},
{
"name": "2pots",
"id": "026fd1a26860499db706bcc529fe82a0",
"complete": true
},
{
"name": "2 fond ",
"id": "0fe10310a35941b3986963699ca54195",
"complete": true
},
{
"name": "terreau",
"id": "233e9dddb9064668bfb11f6f81ff9e41",
"complete": true
},
{
"name": "fromage frais",
"id": "95bfa2b28cda4eb0b1e4206701aa2630",
"complete": true
},
{
"name": "jambon cru",
"id": "9d7fbdc4f13d4209988b658476177d81",
"complete": true
},
{
"name": "wrap",
"id": "4d84892c8767417bbf019bd378a5d21c",
"complete": true
},
{
"name": "saumon",
"id": "bae59fefd2bd424aaf929caab7b3049f",
"complete": true
},
{
"name": "jambon blanc",
"id": "bc8ea8ef705b49d3a9bb7842cd6db54b",
"complete": true
},
{
"name": "thon",
"id": "7635fbfd46e94aacbb3293f765d1061d",
"complete": true
},
{
"name": "ciboulette",
"id": "a0094a31158e4e32a5cf6fb511e6980a",
"complete": true
},
{
"name": "ail",
"id": "5df19ac0aef94f069bbb0eee0fe73c7a",
"complete": true
},
{
"name": "echalotte",
"id": "525468da410c41b9a3c1c8691db3ca76",
"complete": true
},
{
"name": "persil",
"id": "d3c879766f544b329d56cb26fd3ef7b3",
"complete": true
},
{
"name": "comcombre",
"id": "017d0b6ea1914bd6a6d77d012d3947c1",
"complete": true
},
{
"name": "graine sesame",
"id": "e1a34d213ad946239466d23c3025d3f1",
"complete": false
},
{
"name": "vinaigre de vin",
"id": "3b21e435673d4a6e8044407d408d7146",
"complete": true
},
{
"name": "creme fraiche epaise",
"id": "e28cd7092d3a4db4a6fc8163efdb6af7",
"complete": true
},
{
"name": "olive noires",
"id": "186adeaa1f714842a45eb16b00a2bf00",
"complete": true
},
{
"name": "crevette",
"id": "def92909fbb748bcb816154c04f7f0ed",
"complete": true
},
{
"name": "oignon frit",
"id": "0f4a36785fc64c78ae99a1475f222e99",
"complete": true
},
{
"name": "carottes",
"id": "4d296c4157fe4f2b8502a09943345412",
"complete": true
},
{
"name": "avocat",
"id": "46d9f2e912e0473eb8f9b98cca5315ec",
"complete": true
},
{
"name": "fromage chevre",
"id": "7a6a2c857cb34a7e91cad9903c7afbd3",
"complete": false
},
{
"name": "rond 6mm - 8m",
"id": "45e1acb32b6446dca3b9071d2b351b86",
"complete": false
},
{
"name": "rondelle acier",
"id": "9ef5fc4e3ef94d4bad5d2b622169c231",
"complete": false
},
{
"name": "chalumeau",
"id": "ee2913e894424839acbe68251bab796d",
"complete": false "complete": false
} }
] ]
+168 -56
View File
@@ -789,13 +789,17 @@
alias: arret auto ballon 2h alias: arret auto ballon 2h
description: '' description: ''
trigger: trigger:
- platform: state - platform: device
entity_id: type: turned_on
- switch.zb_30a_relay_chauffe_eau device_id: 2d23b7cb9abcf02cfb24d3c138c29f28
from: 'off' entity_id: 0f3c3b69a354e1c462e6176b973ac783
to: 'on' domain: switch
condition: [] condition: []
action: action:
- type: turn_on
device_id: 2d23b7cb9abcf02cfb24d3c138c29f28
entity_id: 0f3c3b69a354e1c462e6176b973ac783
domain: switch
- delay: - delay:
hours: 2 hours: 2
minutes: 40 minutes: 40
@@ -1408,18 +1412,18 @@
minutes: 6 minutes: 6
seconds: 0 seconds: 0
milliseconds: 0 milliseconds: 0
- service: switch.turn_off - data: {}
data: {} action: switch.turn_off
target:
entity_id: switch.prise_salon_uv_z
- service: tts.speak
target: target:
entity_id: switch.prise_uv_zb
- target:
entity_id: tts.google_fr_fr entity_id: tts.google_fr_fr
data: data:
cache: true cache: true
media_player_entity_id: media_player.hp_salon media_player_entity_id: media_player.hp_salon
message: cycle UV terminée message: cycle UV terminée
language: fr language: fr
action: tts.speak
mode: single mode: single
- id: '1691485011308' - id: '1691485011308'
alias: J arrive alias: J arrive
@@ -1456,31 +1460,6 @@
message: l'impression sur l'elegoo est terminée message: l'impression sur l'elegoo est terminée
language: fr language: fr
mode: single mode: single
- id: '1692014714379'
alias: Eclairage hotte cuisine
description: ''
trigger:
- type: present
platform: device
device_id: ea17ed9cfe006b7c2038562d799e9c50
entity_id: ace3e9a593a66c3f854b5d5995b03367
domain: binary_sensor
condition: []
action:
- service: switch.toggle
data: {}
target:
entity_id: switch.prise_eclairage_hotte_cuisine
- delay:
hours: 0
minutes: 0
seconds: 30
milliseconds: 0
- service: switch.toggle
data: {}
target:
entity_id: switch.prise_eclairage_hotte_cuisine
mode: single
- id: '1692313211284' - id: '1692313211284'
alias: restart esphome's alias: restart esphome's
description: '' description: ''
@@ -1498,21 +1477,21 @@
- switch.eclairage_bois_restart - switch.eclairage_bois_restart
mode: single mode: single
- id: '1697147323532' - id: '1697147323532'
alias: 4switchz_1_cuisine alias: bouton_rond_1_cuisine
description: '' description: ''
trigger: trigger:
- platform: device - platform: device
domain: mqtt domain: mqtt
device_id: 43d8325013319e1fd0fb56ab8b63ee7c device_id: 919d9c0cf1d805314bcc290c7b5f4909
type: action type: action
subtype: 1_single subtype: single
condition: [] condition: []
action: action:
- service: light.toggle - data:
data:
brightness_pct: 67 brightness_pct: 67
target: target:
entity_id: light.applique_cuisine entity_id: light.applique_cuisine
action: light.toggle
mode: single mode: single
- id: '1697518688800' - id: '1697518688800'
description: '' description: ''
@@ -2046,22 +2025,6 @@
entity_id: a1c5260977db4e2f04bc3182d6effe2e entity_id: a1c5260977db4e2f04bc3182d6effe2e
domain: switch domain: switch
mode: single mode: single
- id: '1717638094310'
alias: allumer hotte
description: ''
trigger:
- platform: device
domain: mqtt
device_id: 919d9c0cf1d805314bcc290c7b5f4909
type: action
subtype: single
condition: []
action:
- type: toggle
device_id: 872ad88f0351b0e4b70f4ad953a6901c
entity_id: 0c0e6199e0a9c0b62b80e68beb311814
domain: switch
mode: single
- id: '1720289245852' - id: '1720289245852'
alias: poussin eteint alias: poussin eteint
description: '' description: ''
@@ -2294,3 +2257,152 @@
entity_id: 32ea2685688656a175fb0cc49a5cafb2 entity_id: 32ea2685688656a175fb0cc49a5cafb2
domain: switch domain: switch
mode: single mode: single
- id: '1723899733092'
alias: lumiere chambre zb basculer
description: ''
trigger:
- platform: device
domain: mqtt
device_id: cb90c4dd73b557806701e9b702a78d9d
type: action
subtype: single
condition: []
action:
- action: light.toggle
metadata: {}
data: {}
target:
entity_id: light.lumiere_chambre_zb
mode: single
- id: '1723900110653'
alias: commuter eclairage hotte
description: ''
trigger:
- platform: device
domain: mqtt
device_id: 919d9c0cf1d805314bcc290c7b5f4909
type: action
subtype: single
condition: []
action:
- action: switch.toggle
metadata: {}
data: {}
target:
entity_id: switch.prise_hotte_cuisine
mode: single
- id: '1723907961967'
alias: aqara
description: ''
trigger:
- platform: device
domain: mqtt
device_id: bb5d6f72df5f77144822d4aabd8a186a
type: action
subtype: shake
condition: []
action:
- action: light.toggle
metadata: {}
data: {}
target:
entity_id: light.led_bureau_zb
mode: single
- id: '1724174823484'
alias: toogle chambre
description: ''
trigger:
- platform: device
domain: mqtt
device_id: cb90c4dd73b557806701e9b702a78d9d
type: action
subtype: single
condition: []
action:
- type: toggle
device_id: e1beb012d4028827d2f8505dcba88236
entity_id: c02118f907d443dfe518b939ef1cd0eb
domain: light
mode: single
- id: '1724252419859'
alias: lumiere garage
description: ''
trigger:
- type: battery_level
platform: device
device_id: f1a38cf38de33e53a9949d34761e3d4f
entity_id: 1f22d2bc813c337fb4a172a3fc41eef5
domain: sensor
above: 50
condition: []
action:
- type: toggle
device_id: b4dff4d79aef62eaea5c0a76333bc423
entity_id: 1330b7857637c7717b750555479153b7
domain: light
mode: single
- id: '1724325306727'
alias: volet mode persienne
description: ''
trigger:
- platform: state
entity_id:
- input_button.volet_en_persienne
condition: []
action:
- device_id: ef46144b70a1dbb3d2d91a765b386106
domain: cover
entity_id: f43ec655bb13185b833770f4da462532
type: set_position
position: 18
- delay:
hours: 0
minutes: 0
seconds: 15
milliseconds: 0
- device_id: ad59ac98d5e40b70c1f204c114376119
domain: cover
entity_id: 5eaa04d80a8ab739f91e4679b142ba33
type: set_position
position: 15
- delay:
hours: 0
minutes: 0
seconds: 15
milliseconds: 0
- device_id: c1d639eada16ad451b013966e41fc330
domain: cover
entity_id: 4e4294a908fde58ff6d716bc7b0ed10b
type: set_position
position: 15
- delay:
hours: 0
minutes: 0
seconds: 15
milliseconds: 0
- device_id: 79abf759551f86824daf49cfccbb0d68
domain: cover
entity_id: 6ddd0edaeb43d69617564dcae1409711
type: set_position
position: 15
- delay:
hours: 0
minutes: 0
seconds: 15
milliseconds: 0
- device_id: 99b7c7643bea42098472199c8b507f71
domain: cover
entity_id: 7428f45bced98cc1ffe6cd01a6f5cece
type: set_position
position: 18
- delay:
hours: 0
minutes: 0
seconds: 15
milliseconds: 0
- device_id: a7ea77e4d13cd39521f02f788bfb2203
domain: cover
entity_id: 4eee396bb9490b088f0533afe05fdf91
type: set_position
position: 15
mode: single
+5
View File
@@ -158,6 +158,11 @@ recorder:
- sensor.wemos_bureau0_temperature - sensor.wemos_bureau0_temperature
- sensor.blitzortung_lightning_counter - sensor.blitzortung_lightning_counter
- sensor.aubess_cafetiere1_power - sensor.aubess_cafetiere1_power
- sensor.prise_ronde_power
- sensor.prise_ronde_energy
- sensor.smart_plug_2_puissance
- person.gilles
- sensor.qualite_air_salon_zb_co2
#event_types: #event_types:
# - call_service # - call_service
@@ -0,0 +1,285 @@
from datetime import timedelta
from typing import Any, Callable, Optional, TypeVar, cast
import reactivex.operators as ops
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import event
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.device_registry import async_get as async_get_dr
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory
from homeassistant.util.dt import utcnow
from reactivex import Observable, Subject, compose, throw
from reactivex.subject.replaysubject import ReplaySubject
from . import ecoflow as ef
from .ecoflow import receive
from .ecoflow.rxtcp import RxTcpAutoConnection
CONF_PRODUCT = "product"
DISCONNECT_TIME = timedelta(seconds=15)
DOMAIN = "ecoflow"
_PLATFORMS = {
Platform.BINARY_SENSOR,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
}
_T = TypeVar("_T")
async def to_task(src: Observable[_T]):
return await src
async def request(tcp: RxTcpAutoConnection, req: bytes, res: Observable[_T]) -> _T:
t = to_task(res.pipe(
ops.timeout(5, throw(TimeoutError())),
ops.first(),
))
try:
tcp.write(req)
except BaseException as ex:
t.close()
raise ex
return await t
def select_bms(idx: int):
return compose(
ops.filter(lambda x: x[0] == idx),
ops.map(lambda x: cast(dict[str, Any], x[1])),
)
class HassioEcoFlowClient:
__disconnected = None
__extra_connected = False
def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
self.tcp = RxTcpAutoConnection(entry.data[CONF_HOST], ef.PORT)
self.product: int = entry.data[CONF_PRODUCT]
self.serial = entry.unique_id
self.diagnostics = dict[str, dict[str, Any]]()
dr = async_get_dr(hass)
self.device_info_main = DeviceInfo(
identifiers={(DOMAIN, self.serial)},
manufacturer="EcoFlow",
name=entry.title,
)
if mac := entry.data.get(CONF_MAC, None):
self.device_info_main["connections"] = {
(CONNECTION_NETWORK_MAC, mac),
}
self.received = self.tcp.received.pipe(
receive.merge_packet(),
ops.map(receive.decode_packet),
ops.share(),
)
self.pd = self.received.pipe(
ops.filter(receive.is_pd),
ops.map(lambda x: receive.parse_pd(x[3], self.product)),
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
ops.ref_count(),
)
self.ems = self.received.pipe(
ops.filter(receive.is_ems),
ops.map(lambda x: receive.parse_ems(x[3], self.product)),
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
ops.ref_count(),
)
self.inverter = self.received.pipe(
ops.filter(receive.is_inverter),
ops.map(lambda x: receive.parse_inverter(x[3], self.product)),
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
ops.ref_count(),
)
self.mppt = self.received.pipe(
ops.filter(receive.is_mppt),
ops.map(lambda x: receive.parse_mppt(x[3], self.product)),
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
ops.ref_count(),
)
self.bms = self.received.pipe(
ops.filter(receive.is_bms),
ops.map(lambda x: receive.parse_bms(x[3], self.product)),
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
ops.ref_count(),
)
self.dc_in_current_config = self.received.pipe(
ops.filter(receive.is_dc_in_current_config),
ops.map(lambda x: receive.parse_dc_in_current_config(x[3])),
)
self.dc_in_type = self.received.pipe(
ops.filter(receive.is_dc_in_type),
ops.map(lambda x: receive.parse_dc_in_type(x[3])),
)
self.fan_auto = self.received.pipe(
ops.filter(receive.is_fan_auto),
ops.map(lambda x: receive.parse_fan_auto(x[3])),
)
self.lcd_timeout = self.received.pipe(
ops.filter(receive.is_lcd_timeout),
ops.map(lambda x: receive.parse_lcd_timeout(x[3])),
)
self.disconnected = Subject[Optional[int]]()
def _disconnected(*args):
self.__disconnected = None
self.tcp.reconnect()
self.diagnostics.clear()
self.disconnected.on_next(None)
if self.__extra_connected:
self.__extra_connected = False
def reset_timer(*args):
if self.__disconnected:
self.__disconnected()
self.__disconnected = event.async_track_point_in_utc_time(
hass,
_disconnected,
utcnow().replace(microsecond=0) + (DISCONNECT_TIME + timedelta(seconds=1)),
)
def end_timer(ex=None):
self.disconnected.on_next(None)
if ex:
self.disconnected.on_error(ex)
else:
self.disconnected.on_completed()
self.received.subscribe(reset_timer, end_timer, end_timer)
def pd_updated(data: dict[str, Any]):
self.diagnostics["pd"] = data
self.device_info_main["model"] = ef.get_model_name(
self.product, data["model"])
dr.async_get_or_create(
config_entry_id=entry.entry_id,
**self.device_info_main,
)
if self.__extra_connected != ef.has_extra(self.product, data.get("model", None)):
self.__extra_connected = not self.__extra_connected
if not self.__extra_connected:
self.disconnected.on_next(1)
self.pd.subscribe(pd_updated)
def bms_updated(data: tuple[int, dict[str, Any]]):
if "bms" not in self.diagnostics:
self.diagnostics["bms"] = dict[str, Any]()
self.diagnostics["bms"][data[0]] = data[1]
self.bms.subscribe(bms_updated)
def ems_updated(data: dict[str, Any]):
self.diagnostics["ems"] = data
self.ems.subscribe(ems_updated)
def inverter_updated(data: dict[str, Any]):
self.diagnostics["inverter"] = data
self.inverter.subscribe(inverter_updated)
def mppt_updated(data: dict[str, Any]):
self.diagnostics["mppt"] = data
self.mppt.subscribe(mppt_updated)
async def close(self):
self.tcp.close()
await self.tcp.wait_closed()
class EcoFlowBaseEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
_connected = False
def __init__(self, client: HassioEcoFlowClient, bms_id: Optional[int] = None):
self._attr_available = False
self._client = client
self._bms_id = bms_id or 0
self._attr_device_info = client.device_info_main
self._attr_unique_id = client.serial
if bms_id:
self._attr_unique_id += f"-{bms_id}"
async def async_added_to_hass(self):
await super().async_added_to_hass()
self._subscribe(self._client.disconnected, self.__on_disconnected)
def _subscribe(self, src: Observable, func: Callable):
self.async_on_remove(src.subscribe(func).dispose)
def __on_disconnected(self, bms_id: Optional[int]):
if bms_id is not None and self._bms_id != bms_id:
return
self._connected = False
if self._attr_available:
self._attr_available = False
self.async_write_ha_state()
class EcoFlowEntity(EcoFlowBaseEntity):
def __init__(self, client: HassioEcoFlowClient, src: Observable[dict[str, Any]], key: str, name: str, bms_id: Optional[int] = None):
super().__init__(client, bms_id)
self._key = key
self._src = src
self._attr_name = name
self._attr_unique_id += f"-{key.replace('_', '-')}"
async def async_added_to_hass(self):
await super().async_added_to_hass()
self._subscribe(self._src, self.__updated)
def __updated(self, data: dict[str, Any]):
self._attr_available = True
self._on_updated(data)
self.async_write_ha_state()
def _on_updated(self, data: dict[str, Any]):
pass
class EcoFlowConfigEntity(EcoFlowBaseEntity):
_attr_entity_category = EntityCategory.CONFIG
_attr_should_poll = True
def __init__(self, client: HassioEcoFlowClient, key: str, name: str):
super().__init__(client)
self._attr_name = name
self._attr_unique_id += f"-{key.replace('_', '-')}"
async def async_added_to_hass(self):
await super().async_added_to_hass()
self._subscribe(self._client.received, self.__updated)
def __updated(self, data):
if not self._connected:
self._connected = True
self.async_schedule_update_ha_state(True)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
client = HassioEcoFlowClient(hass, entry)
hass.data[DOMAIN][entry.entry_id] = client
hass.config_entries.async_setup_platforms(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
if not await hass.config_entries.async_unload_platforms(entry, _PLATFORMS):
return False
client: HassioEcoFlowClient = hass.data[DOMAIN].pop(entry.entry_id)
await client.close()
return True
@@ -0,0 +1,142 @@
from typing import Any
import reactivex.operators as ops
from homeassistant.components.binary_sensor import (BinarySensorDeviceClass,
BinarySensorEntity)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import (DOMAIN, EcoFlowBaseEntity, EcoFlowEntity, HassioEcoFlowClient,
select_bms)
from .ecoflow import is_delta, is_power_station, is_river
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id]
entities = []
if is_power_station(client.product):
entities.extend([
ChargingEntity(client),
MainErrorEntity(client),
])
if is_delta(client.product):
entities.extend([
ExtraErrorEntity(client, client.bms.pipe(select_bms(
1), ops.share()), "battery_error", "Extra1 status", 1),
ExtraErrorEntity(client, client.bms.pipe(select_bms(
2), ops.share()), "battery_error", "Extra2 status", 2),
InputEntity(client, client.inverter, "ac_in_type", "AC input"),
InputEntity(client, client.mppt, "dc_in_state", "DC input"),
CustomChargeEntity(client, client.inverter,
"ac_in_limit_switch", "AC custom charge speed"),
])
if is_river(client.product):
entities.extend([
ExtraErrorEntity(client, client.bms.pipe(select_bms(
1), ops.share()), "battery_error", "Extra status", 1),
InputEntity(client, client.inverter, "in_type", "Input"),
])
async_add_entities(entities)
class BaseEntity(BinarySensorEntity, EcoFlowEntity):
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = bool(data[self._key])
class ChargingEntity(BinarySensorEntity, EcoFlowBaseEntity):
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
_battery_level = None
_battery_level_max = None
_in_power = None
_out_power = None
def __init__(self, client: HassioEcoFlowClient):
super().__init__(client)
self._attr_name = "Charging"
self._attr_unique_id += "-in-charging"
async def async_added_to_hass(self):
await super().async_added_to_hass()
self._subscribe(self._client.pd, self.__updated)
self._subscribe(self._client.ems, self.__updated)
def __updated(self, data: dict[str, Any]):
self._attr_available = True
self._on_updated(data)
self.async_write_ha_state()
def _on_updated(self, data: dict[str, Any]):
if "in_power" in data:
self._in_power = data["in_power"]
if "out_power" in data:
self._out_power = data["out_power"]
if "battery_level" in data:
self._battery_level = data["battery_level"]
if "battery_level_max" in data:
self._battery_level_max = data["battery_level_max"]
if not self._in_power:
self._attr_is_on = False
elif (self._battery_level is not None) and (self._battery_level_max is not None) and (self._battery_level_max < self._battery_level):
self._attr_is_on = False
elif (self._in_power is not None) and (self._out_power is not None) and (self._in_power <= self._out_power):
self._attr_is_on = False
else:
self._attr_is_on = True
class CustomChargeEntity(BaseEntity):
_attr_entity_category = EntityCategory.CONFIG
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = data[self._key] == 2
class ExtraErrorEntity(BaseEntity):
_attr_device_class = BinarySensorDeviceClass.PROBLEM
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = data[self._key] not in [0, 6]
self._attr_extra_state_attributes = {"code": data[self._key]}
class MainErrorEntity(BinarySensorEntity, EcoFlowBaseEntity):
_attr_device_class = BinarySensorDeviceClass.PROBLEM
def __init__(self, client: HassioEcoFlowClient):
super().__init__(client)
self._attr_name = "Main status"
self._attr_unique_id += "-error"
self._attr_extra_state_attributes = {}
@property
def is_on(self):
return next((True for x in self._attr_extra_state_attributes.values() if x not in [0, 6]), False)
async def async_added_to_hass(self):
await super().async_added_to_hass()
self._subscribe(self._client.pd, self.__updated)
self._subscribe(self._client.ems, self.__updated)
self._subscribe(self._client.inverter, self.__updated)
self._subscribe(self._client.mppt, self.__updated)
def __updated(self, data: dict[str, Any]):
self._attr_available = True
if "ac_error" in data:
self._attr_extra_state_attributes["ac"] = data["ac_error"]
if "battery_main_error" in data:
self._attr_extra_state_attributes["battery"] = data["battery_main_error"]
if "dc_in_error" in data:
self._attr_extra_state_attributes["dc"] = data["dc_in_error"]
if "pd_error" in data:
self._attr_extra_state_attributes["system"] = data["pd_error"]
self.async_write_ha_state()
class InputEntity(BaseEntity):
_attr_device_class = BinarySensorDeviceClass.POWER
@@ -0,0 +1,75 @@
import reactivex.operators as ops
import voluptuous as vol
from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST, CONF_MAC
from . import CONF_PRODUCT, DOMAIN, request
from .ecoflow import PORT, PRODUCTS, receive, send
from .ecoflow.rxtcp import RxTcpAutoConnection
class EcoflowConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
host = None
mac = None
async def _get_serial_main(self):
tcp = RxTcpAutoConnection(self.host, PORT)
received = tcp.received.pipe(
receive.merge_packet(),
ops.map(receive.decode_packet),
ops.filter(receive.is_serial_main),
ops.map(lambda x: receive.parse_serial(x[3])),
)
try:
await tcp.wait_opened()
info = await request(tcp, send.get_serial_main(), received)
finally:
tcp.close()
if info["product"] not in PRODUCTS:
return self.async_abort(reason="product_unsupported")
await self.async_set_unique_id(info["serial"])
self._abort_if_unique_id_configured(updates={
CONF_HOST: self.host,
CONF_MAC: self.mac,
})
return info
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo):
self.host = discovery_info.ip
self.mac = discovery_info.macaddress
await self._get_serial_main()
return self.async_show_form(step_id="user")
async def async_step_user(self, user_input: dict = None):
if user_input:
self.host = user_input.get(CONF_HOST)
errors = {}
if self.host and user_input is not None:
try:
info = await self._get_serial_main()
except TimeoutError:
errors["base"] = "timeout"
else:
pn = PRODUCTS.get(info["product"], "")
if pn != "":
pn += " "
return self.async_create_entry(
title=f'{pn}{info["serial"][-6:]}',
data={
CONF_HOST: self.host,
CONF_MAC: self.mac,
CONF_PRODUCT: info["product"],
},
)
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=vol.Schema({
vol.Required(CONF_HOST, default=self.host): str,
}),
last_step=True,
)
@@ -0,0 +1,24 @@
from datetime import timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import DOMAIN, HassioEcoFlowClient
def _to_serializable(x):
t = type(x)
if t is dict:
x = {y: _to_serializable(x[y]) for y in x}
if t is timedelta:
x = x.__str__()
return x
async def async_get_config_entry_diagnostics(hass: HomeAssistant, entry: ConfigEntry):
client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id]
values = {}
for i in client.diagnostics:
d = client.diagnostics[i]
values[i] = _to_serializable(d)
return values
@@ -0,0 +1,78 @@
PORT = 8055
PRODUCTS = {
5: "RIVER",
7: "RIVER 600 Pro",
12: "RIVER Pro",
13: "DELTA Max",
14: "DELTA Pro",
15: "DELTA Mini",
17: "RIVER Mini",
18: "RIVER Plus",
20: "Smart Generator",
}
_crc8_tab = [0, 7, 14, 9, 28, 27, 18, 21, 56, 63, 54, 49, 36, 35, 42, 45, 112, 119, 126, 121, 108, 107, 98, 101, 72, 79, 70, 65, 84, 83, 90, 93, 224, 231, 238, 233, 252, 251, 242, 245, 216, 223, 214, 209, 196, 195, 202, 205, 144, 151, 158, 153, 140, 139, 130, 133, 168, 175, 166, 161, 180, 179, 186, 189, 199, 192, 201, 206, 219, 220, 213, 210, 255, 248, 241, 246, 227, 228, 237, 234, 183, 176, 185, 190, 171, 172, 165, 162, 143, 136, 129, 134, 147, 148, 157, 154, 39, 32, 41, 46, 59, 60, 53, 50, 31, 24, 17, 22, 3, 4, 13, 10, 87, 80, 89, 94, 75, 76, 69, 66, 111, 104, 97, 102, 115, 116, 125,
122, 137, 142, 135, 128, 149, 146, 155, 156, 177, 182, 191, 184, 173, 170, 163, 164, 249, 254, 247, 240, 229, 226, 235, 236, 193, 198, 207, 200, 221, 218, 211, 212, 105, 110, 103, 96, 117, 114, 123, 124, 81, 86, 95, 88, 77, 74, 67, 68, 25, 30, 23, 16, 5, 2, 11, 12, 33, 38, 47, 40, 61, 58, 51, 52, 78, 73, 64, 71, 82, 85, 92, 91, 118, 113, 120, 127, 106, 109, 100, 99, 62, 57, 48, 55, 34, 37, 44, 43, 6, 1, 8, 15, 26, 29, 20, 19, 174, 169, 160, 167, 178, 181, 188, 187, 150, 145, 152, 159, 138, 141, 132, 131, 222, 217, 208, 215, 194, 197, 204, 203, 230, 225, 232, 239, 250, 253, 244, 243]
_crc16_tab = [0, 49345, 49537, 320, 49921, 960, 640, 49729, 50689, 1728, 1920, 51009, 1280, 50625, 50305, 1088, 52225, 3264, 3456, 52545, 3840, 53185, 52865, 3648, 2560, 51905, 52097, 2880, 51457, 2496, 2176, 51265, 55297, 6336, 6528, 55617, 6912, 56257, 55937, 6720, 7680, 57025, 57217, 8000, 56577, 7616, 7296, 56385, 5120, 54465, 54657, 5440, 55041, 6080, 5760, 54849, 53761, 4800, 4992, 54081, 4352, 53697, 53377, 4160, 61441, 12480, 12672, 61761, 13056, 62401, 62081, 12864, 13824, 63169, 63361, 14144, 62721, 13760, 13440, 62529, 15360, 64705, 64897, 15680, 65281, 16320, 16000, 65089, 64001, 15040, 15232, 64321, 14592, 63937, 63617, 14400, 10240, 59585, 59777, 10560, 60161, 11200, 10880, 59969, 60929, 11968, 12160, 61249, 11520, 60865, 60545, 11328, 58369, 9408, 9600, 58689, 9984, 59329, 59009, 9792, 8704, 58049, 58241, 9024, 57601, 8640, 8320, 57409, 40961, 24768,
24960, 41281, 25344, 41921, 41601, 25152, 26112, 42689, 42881, 26432, 42241, 26048, 25728, 42049, 27648, 44225, 44417, 27968, 44801, 28608, 28288, 44609, 43521, 27328, 27520, 43841, 26880, 43457, 43137, 26688, 30720, 47297, 47489, 31040, 47873, 31680, 31360, 47681, 48641, 32448, 32640, 48961, 32000, 48577, 48257, 31808, 46081, 29888, 30080, 46401, 30464, 47041, 46721, 30272, 29184, 45761, 45953, 29504, 45313, 29120, 28800, 45121, 20480, 37057, 37249, 20800, 37633, 21440, 21120, 37441, 38401, 22208, 22400, 38721, 21760, 38337, 38017, 21568, 39937, 23744, 23936, 40257, 24320, 40897, 40577, 24128, 23040, 39617, 39809, 23360, 39169, 22976, 22656, 38977, 34817, 18624, 18816, 35137, 19200, 35777, 35457, 19008, 19968, 36545, 36737, 20288, 36097, 19904, 19584, 35905, 17408, 33985, 34177, 17728, 34561, 18368, 18048, 34369, 33281, 17088, 17280, 33601, 16640, 33217, 32897, 16448]
def calcCrc8(data: bytes):
crc = 0
for i3 in range(len(data)):
crc = _crc8_tab[(crc ^ data[i3]) & 255]
return crc.to_bytes(1, "little")
def calcCrc16(data: bytes):
crc = 0
for i3 in range(len(data)):
crc = _crc16_tab[(crc ^ data[i3]) & 255] ^ (crc >> 8)
return crc.to_bytes(2, "little")
def get_model_name(product: int, model: int):
if product == 5 and model == 2:
return "RIVER Max"
elif product == 18 and model == 2:
return "RIVER Max Plus"
else:
return PRODUCTS.get(product, None)
def has_extra(product: int, model: int):
if product in [5, 12]:
return model == 2
return False
def has_light(product: int):
return product in [5, 7, 12, 18]
def is_delta(product: int):
return 12 < product < 16
def is_delta_max(product: int):
return product == 13
def is_delta_mini(product: int):
return product == 15
def is_delta_pro(product: int):
return product == 14
def is_power_station(product: int):
return is_delta(product) or is_river(product) or is_river_mini(product)
def is_river(product: int):
return product in [5, 7, 12, 18]
def is_river_mini(product: int):
return product == 17
@@ -0,0 +1,558 @@
import struct
from datetime import timedelta
from typing import Any, Callable, Iterable, Optional, TypedDict, cast
from reactivex import Observable, Observer
from . import calcCrc8, calcCrc16, is_delta, is_river
class Serial(TypedDict):
chk_val: int
product: int
product_detail: int
model: int
serial: str
cpu_id: str
def _merge_packet(obs: Observable[Optional[bytes]]):
def func(sub: Observer[bytes], sched=None):
x = b''
def next(rcv: Optional[bytes]):
nonlocal x
if rcv is None:
x = b''
return
x += rcv
while len(x) >= 18:
if x[:2] != b'\xaa\x02':
x = x[1:]
continue
size = int.from_bytes(x[2:4], 'little')
if 18 + size > len(x):
return
if calcCrc8(x[:4]) != x[4:5]:
x = x[2:]
continue
if calcCrc16(x[:16 + size]) != x[16 + size:18 + size]:
x = x[2:]
continue
sub.on_next(x[:18 + size])
x = x[18 + size:]
return obs.subscribe(next, sub.on_error, sub.on_completed, scheduler=sched)
return Observable[bytes](func)
def _parse_dict(d: bytes, types: Iterable[tuple[str, int, Callable[[bytes], Any]]]):
res = dict[str, Any]()
idx = 0
_len = len(d)
for (name, size, fn) in types:
if name is not None:
res[name] = fn(d[idx:idx + size])
idx += size
if idx >= _len:
break
return res
def _to_float(d: bytes) -> float:
return struct.unpack("<f", d)[0]
def _to_int(d: bytes):
return int.from_bytes(d, "little")
def _to_int_ex(div: int = 1):
def f(d: bytes):
v = _to_int(d)
if v is None:
return None
v /= div
return v
return f
def _to_timedelta_min(d: bytes):
return timedelta(minutes=int.from_bytes(d, "little"))
def _to_timedelta_sec(d: bytes):
return timedelta(seconds=int.from_bytes(d, "little"))
def _to_utf8(d: bytes):
try:
return d.decode("utf-8")
except:
return None
def _to_ver(data: Iterable[int]):
return ".".join(str(i) for i in data)
def _to_ver_reversed(data: Iterable[int]):
return _to_ver(reversed(data))
def decode_packet(x: bytes):
size = int.from_bytes(x[2:4], 'little')
args = x[16:16 + size]
if ((x[5] >> 5) & 3) == 1:
# Deobfuscation
args = bytes(v ^ x[6] for v in args)
return (x[12], x[14], x[15], args)
def is_bms(x: tuple[int, int, int]):
return x[0:3] == (3, 32, 50) or x[0:3] == (6, 32, 2) or x[0:3] == (6, 32, 50)
def is_dc_in_current_config(x: tuple[int, int, int]):
return x[0:3] == (4, 32, 72) or x[0:3] == (5, 32, 72)
def is_dc_in_type(x: tuple[int, int, int]):
return x[0:3] == (4, 32, 68) or x[0:3] == (5, 32, 82)
def is_ems(x: tuple[int, int, int]):
return x[0:3] == (3, 32, 2)
def is_fan_auto(x: tuple[int, int, int]):
return x[0:3] == (4, 32, 74)
def is_inverter(x: tuple[int, int, int]):
return x[0:3] == (4, 32, 2)
def is_lcd_timeout(x: tuple[int, int, int]):
return x[0:3] == (2, 32, 40)
def is_mppt(x: tuple[int, int, int]):
return x[0:3] == (5, 32, 2)
def is_pd(x: tuple[int, int, int]):
return x[0:3] == (2, 32, 2)
def is_serial_main(x: tuple[int, int, int]):
return x[0] in [2, 11] and x[1:3] == (1, 65)
def is_serial_extra(x: tuple[int, int, int]):
return x[0:3] == (6, 1, 65)
def parse_bms(d: bytes, product: int):
if is_delta(product):
return parse_bms_delta(d)
if is_river(product):
return parse_bms_river(d)
return (0, {})
def parse_bms_delta(d: bytes):
val = _parse_dict(d, [
("num", 1, _to_int),
("battery_type", 1, _to_int),
("battery_cell_id", 1, _to_int),
("battery_error", 4, _to_int),
("battery_version", 4, _to_ver_reversed),
("battery_level", 1, _to_int),
("battery_voltage", 4, _to_int_ex(div=1000)),
("battery_current", 4, _to_int),
("battery_temp", 1, _to_int),
("_open_bms_idx", 1, _to_int),
("battery_capacity_design", 4, _to_int),
("battery_capacity_remain", 4, _to_int),
("battery_capacity_full", 4, _to_int),
("battery_cycles", 4, _to_int),
("_soh", 1, _to_int),
("battery_voltage_max", 2, _to_int_ex(div=1000)),
("battery_voltage_min", 2, _to_int_ex(div=1000)),
("battery_temp_max", 1, _to_int),
("battery_temp_min", 1, _to_int),
("battery_mos_temp_max", 1, _to_int),
("battery_mos_temp_min", 1, _to_int),
("battery_fault", 1, _to_int),
("_sys_stat_reg", 1, _to_int),
("_tag_chg_current", 4, _to_int),
("battery_level_f32", 4, _to_float),
("battery_in_power", 4, _to_int),
("battery_out_power", 4, _to_int),
("battery_remain", 4, _to_timedelta_min),
])
return (cast(int, val.pop("num")), val)
def parse_bms_river(d: bytes):
return (1, _parse_dict(d, [
("battery_error", 4, _to_int),
("battery_version", 4, _to_ver_reversed),
("battery_level", 1, _to_int),
("battery_voltage", 4, _to_int_ex(div=1000)),
("battery_current", 4, _to_int),
("battery_temp", 1, _to_int),
("battery_capacity_remain", 4, _to_int),
("battery_capacity_full", 4, _to_int),
("battery_cycles", 4, _to_int),
("ambient_mode", 1, _to_int),
("ambient_animate", 1, _to_int),
("ambient_color", 4, list),
("ambient_brightness", 1, _to_int),
]))
def parse_dc_in_current_config(d: bytes):
return int.from_bytes(d[:4], "little")
def parse_dc_in_type(d: bytes):
return d[1]
def parse_ems(d: bytes, product: int):
if is_delta(product):
return parse_ems_delta(d)
if is_river(product):
return parse_ems_river(d)
# if is_river_mini(product):
# return parse_ems_river_mini(d)
return {}
def parse_ems_delta(d: bytes):
return _parse_dict(d, [
("_state_charge", 1, _to_int),
("_chg_cmd", 1, _to_int),
("_dsg_cmd", 1, _to_int),
("battery_main_voltage", 4, _to_int_ex(div=1000)),
("battery_main_current", 4, _to_int_ex(div=1000)),
("_fan_level", 1, _to_int),
("battery_level_max", 1, _to_int),
("model", 1, _to_int),
("battery_main_level", 1, _to_int),
("_flag_open_ups", 1, _to_int),
("battery_main_warning", 1, _to_int),
("battery_remain_charge", 4, _to_timedelta_min),
("battery_remain_discharge", 4, _to_timedelta_min),
("battery_main_normal", 1, _to_int),
("battery_main_level_f32", 4, _to_float),
("_is_connect", 3, _to_int),
("_max_available_num", 1, _to_int),
("_open_bms_idx", 1, _to_int),
("battery_main_voltage_min", 4, _to_int_ex(div=1000)),
("battery_main_voltage_max", 4, _to_int_ex(div=1000)),
("battery_level_min", 1, _to_int),
("generator_level_start", 1, _to_int),
("generator_level_stop", 1, _to_int),
])
def parse_ems_river(d: bytes):
return _parse_dict(d, [
("battery_main_error", 4, _to_int),
("battery_main_version", 4, _to_ver_reversed),
("battery_main_level", 1, _to_int),
("battery_main_voltage", 4, _to_int_ex(div=1000)),
("battery_main_current", 4, _to_int),
("battery_main_temp", 1, _to_int),
("_open_bms_idx", 1, _to_int),
("battery_capacity_remain", 4, _to_int),
("battery_capacity_full", 4, _to_int),
("battery_cycles", 4, _to_int),
("battery_level_max", 1, _to_int),
("battery_main_voltage_max", 2, _to_int_ex(div=1000)),
("battery_main_voltage_min", 2, _to_int_ex(div=1000)),
("battery_main_temp_max", 1, _to_int),
("battery_main_temp_min", 1, _to_int),
("mos_temp_max", 1, _to_int),
("mos_temp_min", 1, _to_int),
("battery_main_fault", 1, _to_int),
("_bq_sys_stat_reg", 1, _to_int),
("_tag_chg_amp", 4, _to_int),
])
# def parse_ems_river_mini(d: bytes):
# pass
def parse_fan_auto(d: bytes):
return d[0] == 1
def parse_inverter(d: bytes, product: int):
if is_delta(product):
return parse_inverter_delta(d)
if is_river(product):
return parse_inverter_river(d)
# if is_river_mini(product):
# return parse_pd_river_mini(d)
return {}
def parse_inverter_delta(d: bytes):
return _parse_dict(d, [
("ac_error", 4, _to_int),
("ac_version", 4, _to_ver_reversed),
("ac_in_type", 1, _to_int),
("ac_in_power", 2, _to_int),
("ac_out_power", 2, _to_int),
("ac_type", 1, _to_int),
("ac_out_voltage", 4, _to_int_ex(div=1000)),
("ac_out_current", 4, _to_int_ex(div=1000)),
("ac_out_freq", 1, _to_int),
("ac_in_voltage", 4, _to_int_ex(div=1000)),
("ac_in_current", 4, _to_int_ex(div=1000)),
("ac_in_freq", 1, _to_int),
("ac_out_temp", 2, _to_int),
("dc_in_voltage", 4, _to_int),
("dc_in_current", 4, _to_int),
("ac_in_temp", 2, _to_int),
("fan_state", 1, _to_int),
("ac_out_state", 1, _to_int),
("ac_out_xboost", 1, _to_int),
("ac_out_voltage_config", 4, _to_int_ex(div=1000)),
("ac_out_freq_config", 1, _to_int),
("fan_config", 1, _to_int),
("ac_in_pause", 1, _to_int),
("ac_in_limit_switch", 1, _to_int),
("ac_in_limit_max", 2, _to_int),
("ac_in_limit_custom", 2, _to_int),
("ac_out_timeout", 2, _to_int),
])
def parse_inverter_river(d: bytes):
return _parse_dict(d, [
("ac_error", 4, _to_int),
("ac_version", 4, _to_ver_reversed),
("in_type", 1, _to_int),
("in_power", 2, _to_int),
("ac_out_power", 2, _to_int),
("ac_type", 1, _to_int),
("ac_out_voltage", 4, _to_int_ex(div=1000)),
("ac_out_current", 4, _to_int_ex(div=1000)),
("ac_out_freq", 1, _to_int),
("ac_in_voltage", 4, _to_int_ex(div=1000)),
("ac_in_current", 4, _to_int_ex(div=1000)),
("ac_in_freq", 1, _to_int),
("ac_out_temp", 1, _to_int),
("dc_in_voltage", 4, _to_int_ex(div=1000)),
("dc_in_current", 4, _to_int_ex(div=1000)),
("ac_in_temp", 1, _to_int),
("fan_state", 1, _to_int),
("ac_out_state", 1, _to_int),
("ac_out_xboost", 1, _to_int),
("ac_out_voltage_config", 4, _to_int_ex(div=1000)),
("ac_out_freq_config", 1, _to_int),
("ac_in_slow", 1, _to_int),
("ac_out_timeout", 2, _to_int),
("fan_config", 1, _to_int),
])
def parse_lcd_timeout(d: bytes):
return int.from_bytes(d[1:3], "little")
def parse_mppt(d: bytes, product: int):
if is_delta(product):
return parse_mppt_delta(d)
return {}
def parse_mppt_delta(d: bytes):
return _parse_dict(d, [
("dc_in_error", 4, _to_int),
("dc_in_version", 4, _to_ver_reversed),
("dc_in_voltage", 4, _to_int_ex(div=10)),
("dc_in_current", 4, _to_int_ex(div=100)),
("dc_in_power", 2, _to_int_ex(div=10)),
("_volt_?_out", 4, _to_int),
("_curr_?_out", 4, _to_int),
("_watts_?_out", 2, _to_int),
("dc_in_temp", 2, _to_int),
("dc_in_type", 1, _to_int),
("dc_in_type_config", 1, _to_int),
("_dc_in_type", 1, _to_int),
("dc_in_state", 1, _to_int),
("anderson_out_voltage", 4, _to_int),
("anderson_out_current", 4, _to_int),
("anderson_out_power", 2, _to_int),
("car_out_voltage", 4, _to_int_ex(div=10)),
("car_out_current", 4, _to_int_ex(div=100)),
("car_out_power", 2, _to_int_ex(div=10)),
("car_out_temp", 2, _to_int),
("car_out_state", 1, _to_int),
("dc24_temp", 2, _to_int),
("dc24_state", 1, _to_int),
("dc_in_pause", 1, _to_int),
("_dc_in_switch", 1, _to_int),
("_dc_in_limit_max", 2, _to_int),
("_dc_in_limit_custom", 2, _to_int),
])
def parse_pd(d: bytes, product: int):
if is_delta(product):
return parse_pd_delta(d)
if is_river(product):
return parse_pd_river(d)
# if is_river_mini(product):
# return parse_pd_river_mini(d)
return {}
def parse_pd_delta(d: bytes):
return _parse_dict(d, [
("model", 1, _to_int),
("pd_error", 4, _to_int),
("pd_version", 4, _to_ver_reversed),
("wifi_version", 4, _to_ver_reversed),
("wifi_autorecovery", 1, _to_int),
("battery_level", 1, _to_int),
("out_power", 2, _to_int),
("in_power", 2, _to_int),
("remain_display", 4, _to_timedelta_min),
("beep", 1, _to_int),
("_watts_anderson_out", 1, _to_int),
("usb_out1_power", 1, _to_int),
("usb_out2_power", 1, _to_int),
("usbqc_out1_power", 1, _to_int),
("usbqc_out2_power", 1, _to_int),
("typec_out1_power", 1, _to_int),
("typec_out2_power", 1, _to_int),
("typec_out1_temp", 1, _to_int),
("typec_out2_temp", 1, _to_int),
("car_out_state", 1, _to_int),
("car_out_power", 1, _to_int),
("car_out_temp", 1, _to_int),
("standby_timeout", 2, _to_int),
("lcd_timeout", 2, _to_int),
("lcd_brightness", 1, _to_int),
("car_in_energy", 4, _to_int),
("mppt_in_energy", 4, _to_int),
("ac_in_energy", 4, _to_int),
("car_out_energy", 4, _to_int),
("ac_out_energy", 4, _to_int),
("usb_time", 4, _to_timedelta_sec),
("typec_time", 4, _to_timedelta_sec),
("car_out_time", 4, _to_timedelta_sec),
("ac_out_time", 4, _to_timedelta_sec),
("ac_in_time", 4, _to_timedelta_sec),
("car_in_time", 4, _to_timedelta_sec),
("mppt_time", 4, _to_timedelta_sec),
(None, 2, None),
("_ext_rj45", 1, _to_int),
("_ext_infinity", 1, _to_int),
])
def parse_pd_river(d: bytes):
return _parse_dict(d, [
("model", 1, _to_int),
("pd_error", 4, _to_int),
("pd_version", 4, _to_ver_reversed),
("battery_level", 1, _to_int),
("out_power", 2, _to_int),
("in_power", 2, _to_int),
("remain_display", 4, _to_timedelta_min),
("car_out_state", 1, _to_int),
("light_state", 1, _to_int),
("beep", 1, _to_int),
("typec_out1_power", 1, _to_int),
("usb_out1_power", 1, _to_int),
("usb_out2_power", 1, _to_int),
("usbqc_out1_power", 1, _to_int),
("car_out_power", 1, _to_int),
("light_power", 1, _to_int),
("typec_out1_temp", 1, _to_int),
("car_out_temp", 1, _to_int),
("standby_timeout", 2, _to_int),
("car_in_energy", 4, _to_int),
("mppt_in_energy", 4, _to_int),
("ac_in_energy", 4, _to_int),
("car_out_energy", 4, _to_int),
("ac_out_energy", 4, _to_int),
("usb_time", 4, _to_timedelta_sec),
("usbqc_time", 4, _to_timedelta_sec),
("typec_time", 4, _to_timedelta_sec),
("car_out_time", 4, _to_timedelta_sec),
("ac_out_time", 4, _to_timedelta_sec),
("car_in_time", 4, _to_timedelta_sec),
("mppt_time", 4, _to_timedelta_sec),
])
# def parse_pd_river_mini(d: bytes):
# return _parse_dict(d, [
# ("model", 1, _to_int),
# ("pd_error", 4, _to_int),
# ("pd_version", 4, _to_ver_reversed),
# ("wifi_version", 4, _to_ver_reversed),
# ("wifi_autorecovery", 1,),
# ("soc_sum", 1, _to_int),
# ("watts_out_sum", 2, _to_int),
# ("watts_in_sum", 2, _to_int),
# ("remain_time", 4, _to_int),
# ("beep", 1, _to_int),
# ("dc_out", 1, _to_int),
# ("usb1_watts", 1, _to_int),
# ("usb2_watts", 1, _to_int),
# ("usbqc1_watts", 1, _to_int),
# ("usbqc2_watts", 1, _to_int),
# ("typec1_watts", 1, _to_int),
# ("typec2_watts", 1, _to_int),
# ("typec1_temp", 1, _to_int),
# ("typec2_temp", 1, _to_int),
# ("dc_out_watts", 1, _to_int),
# ("car_out_temp", 1, _to_int),
# ("standby_timeout", 2, _to_int),
# ("lcd_sec", 2, _to_int),
# ("lcd_brightness", 1, _to_int),
# ("chg_power_dc", 4, _to_int),
# ("chg_power_mppt", 4, _to_int),
# ("chg_power_ac", 4, _to_int),
# ("dsg_power_dc", 4, _to_int),
# ("dsg_power_ac", 4, _to_int),
# ("usb_used_time", 4, _to_int),
# ("usbqc_used_time", 4, _to_int),
# ("typec_used_time", 4, _to_int),
# ("dc_out_used_time", 4, _to_int),
# ("ac_out_used_time", 4, _to_int),
# ("dc_in_used_time", 4, _to_int),
# ("mppt_used_time", 4, _to_int),
# (None, 5, None),
# ("sys_chg_flag", 1, _to_int),
# ("wifi_rssi", 1, _to_int),
# ("wifi_watts", 1, _to_int),
# ])
def parse_serial(d: bytes) -> Serial:
return _parse_dict(d, [
("chk_val", 4, _to_int),
("product", 1, _to_int),
(None, 1, None),
("product_detail", 1, _to_int),
("model", 1, _to_int),
("serial", 15, _to_utf8),
(None, 1, None),
("cpu_id", 12, _to_utf8),
])
def merge_packet():
return _merge_packet
@@ -0,0 +1,84 @@
from asyncio import Future, create_task, open_connection, sleep
from logging import getLogger
from typing import Optional
from reactivex import Subject
_LOGGER = getLogger(__name__)
class RxTcpAutoConnection:
__rx = None
__tx = None
def __init__(self, host: str, port: int):
self.host = host
self.port = port
self.received = Subject[Optional[bytes]]()
self.__is_open = True
self.__task = create_task(self.__loop())
self.__opened = Future()
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
self.close()
await self.wait_closed()
def close(self):
self.__is_open = False
if self.__rx:
self.__rx.feed_eof()
async def drain(self):
await self.__tx.drain()
def reconnect(self):
if self.__rx:
self.__rx.feed_eof()
async def wait_closed(self):
try:
await self.__task
except:
pass
try:
await self.__tx.wait_closed()
except:
pass
async def wait_opened(self):
await self.__opened
def write(self, data: bytes):
self.__tx.write(data)
async def __loop(self):
while self.__is_open:
_LOGGER.debug(f"connecting {self.host}")
try:
(self.__rx, self.__tx) = await open_connection(self.host, self.port)
except Exception as ex:
_LOGGER.debug(ex)
await sleep(1)
continue
_LOGGER.debug(f"connected {self.host}")
if not self.__opened.done():
self.__opened.set_result(None)
try:
while not self.__rx.at_eof():
data = await self.__rx.read(1024)
if data:
self.received.on_next(data)
except Exception as ex:
if type(ex) is not TimeoutError:
_LOGGER.exception(ex)
except BaseException as ex:
self.received.on_error(ex)
return
finally:
self.__rx.feed_eof()
self.__tx.close()
self.received.on_next(None)
self.received.on_completed()
@@ -0,0 +1,193 @@
from typing import Optional
from . import calcCrc8, calcCrc16, is_delta, is_river_mini
NO_USB_SWITCH = {5, 7, 12, 14, 15, 18}
def _btoi(b: Optional[bool]):
if b is None:
return 255
return 1 if b else 0
def build2(dst: int, cmd_set: int, cmd_id: int, data: bytes = b''):
b = bytes([170, 2])
b += len(data).to_bytes(2, "little")
b += calcCrc8(b)
b += bytes([13, 0, 0, 0, 0, 0, 0, 32, dst, cmd_set, cmd_id])
b += data
b += calcCrc16(b)
return b
def get_product_info(dst: int):
return build2(dst, 1, 5)
def get_cpu_id():
return build2(2, 1, 64)
def get_serial_main():
return build2(2, 1, 65)
def get_pd():
return build2(2, 32, 2, b'\0')
def reset():
return build2(2, 32, 3)
def set_standby_timeout(value: int):
return build2(2, 32, 33, value.to_bytes(2, "little"))
def set_usb(enable: bool):
return build2(2, 32, 34, bytes([1 if enable else 0]))
def set_light(product: int, value: int):
return build2(2, 32, 35, bytes([value]))
def set_dc_out(product: int, enable: bool):
if is_delta(product):
cmd = (5, 32, 81)
elif product == 20:
cmd = (8, 8, 3)
elif product in [5, 7, 12, 18]:
cmd = (2, 32, 34)
else:
cmd = (2, 32, 37)
return build2(*cmd, bytes([1 if enable else 0]))
def set_beep(enable: bool):
return build2(2, 32, 38, bytes([0 if enable else 1]))
def set_lcd(product: int, time: int = 0xFFFF, light: int = 255):
arg = time.to_bytes(2, "little")
if is_delta(product) or is_river_mini(product):
arg += bytes([light])
return build2(2, 32, 39, arg)
def get_lcd():
return build2(2, 32, 40)
def close(value: int):
return build2(2, 32, 41, value.to_bytes(2, "little"))
def get_ems_main():
return build2(3, 32, 2)
def set_level_max(product: int, value: int):
dst = 4 if product == 17 else 3
return build2(dst, 32, 49, bytes([value]))
def set_level_min(value: int):
return build2(3, 32, 51, bytes([value]))
def set_generate_start(value: int):
return build2(3, 32, 52, bytes([value]))
def set_generate_stop(value: int):
return build2(3, 32, 53, bytes([value]))
def get_inverter():
return build2(4, 32, 2)
def set_ac_in_slow(value: bool):
return build2(4, 32, 65, bytes([_btoi(value)]))
def set_ac_out(product: int, enable: bool = None, xboost: bool = None, freq: int = 255):
if product == 20:
cmd = (8, 8, 2)
arg = [_btoi(enable)]
else:
cmd = (4, 32, 66)
arg = [_btoi(enable), _btoi(xboost), 255, 255, 255, 255, freq]
return build2(*cmd, bytes(arg))
def set_dc_in_type(product: int, value: int):
if is_delta(product):
cmd = (5, 32, 82)
else:
cmd = (4, 32, 67)
return build2(*cmd, bytes([value]))
def get_dc_in_type(product: int):
if is_delta(product):
cmd = (5, 32, 82)
else:
cmd = (4, 32, 68)
return build2(*cmd, bytes([0]))
def set_ac_in_limit(watts: int = 0xFFFF, pause: bool = None):
arg = bytes([255, 255])
arg += watts.to_bytes(2, "little")
arg += bytes([_btoi(pause)])
return build2(4, 32, 69, arg)
def set_dc_in_current(product: int, value: int):
dst = 5 if is_delta(product) else 4
return build2(dst, 32, 71, value.to_bytes(4, "little"))
def get_dc_in_current(product: int):
dst = 5 if is_delta(product) else 4
return build2(dst, 32, 72)
def set_fan_auto(product: int, value: bool):
return build2(4, 32, 73, bytes([1 if value else 3]))
def get_fan_auto():
return build2(4, 32, 74)
def get_lab():
return build2(4, 32, 84)
def set_lab(value: int):
return build2(4, 32, 84, bytes([value]))
def set_ac_timeout(value: int):
return build2(4, 32, 153, value.to_bytes(2, "little"))
def get_serial_extra():
return build2(6, 1, 65)
def get_ems_extra():
return build2(6, 32, 2)
def set_ambient(mode: int = 255, animate: int = 255, color=(255, 255, 255, 255), brightness=255):
arg = [mode, animate, *color, brightness]
return build2(6, 32, 97, bytes(arg))
def _set_watt(value: int):
return build2(8, 8, 7, value.to_bytes(2, "little"))
+100
View File
@@ -0,0 +1,100 @@
from typing import Any
from homeassistant.components.light import (ColorMode, LightEntity,
LightEntityFeature)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, EcoFlowEntity, HassioEcoFlowClient, select_bms
from .ecoflow import is_river, send
_EFFECTS = ["Low", "High", "SOS"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id]
entities = []
if is_river(client.product):
entities.extend([
LedEntity(client, client.pd, "light_state", "Light"),
])
if client.product == 5: # RIVER Max
entities.extend([
AmbientEntity(client, client.bms.pipe(
select_bms(1)), "ambient", "Ambient light", 1),
])
async_add_entities(entities)
class AmbientEntity(LightEntity, EcoFlowEntity):
_attr_effect_list = ["Default", "Breathe", "Flow", "Dynamic", "Rainbow"]
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:led-strip"
_attr_supported_color_modes = {ColorMode.RGB, ColorMode.BRIGHTNESS}
_attr_supported_features = LightEntityFeature.EFFECT
_last_mode = 1
async def async_turn_off(self, **kwargs):
self._client.tcp.write(send.set_ambient(0))
async def async_turn_on(self, brightness=None, rgb_color=None, effect=None, **kwargs):
if brightness is None:
brightness = 255
else:
brightness = int(brightness * 100 / 255)
if rgb_color is None:
rgb_color = (255, 255, 255, 255)
else:
rgb_color = list[int](rgb_color)
rgb_color.append(0)
if effect is None:
effect = 255
else:
effect = self._attr_effect_list.index(effect)
self._client.tcp.write(send.set_ambient(
self._last_mode, effect, rgb_color, brightness))
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = data["ambient_mode"] != 0
self._attr_brightness = int(data["ambient_brightness"] * 255 / 100)
if self._attr_is_on:
self._last_mode = data["ambient_mode"]
self._attr_effect = self._attr_effect_list[data["ambient_animate"]]
self._attr_color_mode = ColorMode.BRIGHTNESS if data[
"ambient_animate"] > 1 else ColorMode.RGB
else:
self._attr_effect = None
self._attr_color_mode = None
self._attr_rgb_color = data["ambient_color"][0:3]
class LedEntity(LightEntity, EcoFlowEntity):
_attr_effect = _EFFECTS[0]
_attr_effect_list = _EFFECTS
_attr_supported_color_modes = {ColorMode.ONOFF}
_attr_supported_features = LightEntityFeature.EFFECT
def _on_updated(self, data: dict[str, Any]):
value = data[self._key]
if value != 0:
self._attr_is_on = True
self._attr_effect = _EFFECTS[value - 1]
else:
self._attr_is_on = False
self._attr_effect = None
async def async_turn_off(self, **kwargs):
self._client.tcp.write(send.set_light(self._client.product, 0))
async def async_turn_on(self, effect: str = None, **kwargs):
if not effect:
effect = self.effect or _EFFECTS[0]
self._client.tcp.write(send.set_light(
self._client.product, _EFFECTS.index(effect) + 1))
@@ -0,0 +1,21 @@
{
"domain": "ecoflow",
"name": "Ecoflow",
"version": "2.1",
"documentation": "https://github.com/vwt12eh8/hassio-ecoflow",
"issue_tracker": "https://github.com/vwt12eh8/hassio-ecoflow/issues",
"requirements": [
"reactivex"
],
"config_flow": true,
"codeowners": [
"@vwt12eh8"
],
"dhcp": [
{
"hostname": "ecoflow_*",
"macaddress": "*"
}
],
"iot_class": "local_push"
}
+175
View File
@@ -0,0 +1,175 @@
from typing import Any
from homeassistant.components.number import NumberEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ELECTRIC_CURRENT_AMPERE, PERCENTAGE, POWER_WATT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import (DOMAIN, EcoFlowConfigEntity, EcoFlowEntity, HassioEcoFlowClient,
request)
from .ecoflow import (is_delta, is_delta_max, is_delta_mini, is_delta_pro,
is_power_station, send)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id]
entities = []
if is_power_station(client.product):
entities.extend([
DcInCurrentEntity(client, "dc_in_current_config",
"Car input"),
MaxLevelEntity(client, client.ems,
"battery_level_max", "Charge level"),
])
if is_delta(client.product):
entities.extend([
ChargeWattsEntity(client, client.inverter,
"ac_in_limit_custom", "AC charge speed"),
LcdBrightnessEntity(client, client.pd,
"lcd_brightness", "Screen brightness"),
MinLevelEntity(client, client.ems,
"battery_level_min", "Discharge level"),
])
if is_delta_pro(client.product):
entities.extend([
GenerateStartEntity(
client, client.ems, "generator_level_start", "Smart generator auto on"),
GenerateStopEntity(
client, client.ems, "generator_level_stop", "Smart generator auto off"),
])
async_add_entities(entities)
class BaseEntity(NumberEntity, EcoFlowEntity):
_attr_entity_category = EntityCategory.CONFIG
def _on_updated(self, data: dict[str, Any]):
self._attr_native_value = data[self._key]
class ChargeWattsEntity(BaseEntity):
_attr_icon = "mdi:car-speed-limiter"
_attr_native_max_value = 1500
_attr_native_min_value = 200
_attr_native_step = 100
_attr_native_unit_of_measurement = POWER_WATT
async def async_set_native_value(self, value: float):
self._client.tcp.write(send.set_ac_in_limit(int(value)))
def _on_updated(self, data: dict[str, Any]):
super()._on_updated(data)
voltage: float = data["ac_out_voltage_config"]
if is_delta_max(self._client.product):
if self._client.serial.startswith("DD"):
self._attr_native_max_value = 1600
elif voltage >= 220:
self._attr_native_max_value = 2000
elif voltage >= 120:
self._attr_native_max_value = 1800
elif voltage >= 110:
self._attr_native_max_value = 1650
else:
self._attr_native_max_value = 1500
elif is_delta_pro(self._client.product):
if voltage >= 240:
self._attr_native_max_value = 3000
elif voltage >= 230:
self._attr_native_max_value = 2900
elif voltage >= 220:
self._attr_native_max_value = 2200
elif voltage >= 120:
self._attr_native_max_value = 1800
elif voltage >= 110:
self._attr_native_max_value = 1650
else:
self._attr_native_max_value = 1500
elif is_delta_mini(self._client.product):
self._attr_native_max_value = 900
else:
self._attr_native_max_value = 1500
class DcInCurrentEntity(NumberEntity, EcoFlowConfigEntity):
_attr_icon = "mdi:car-speed-limiter"
_attr_native_max_value = 8
_attr_native_min_value = 4
_attr_native_step = 2
_attr_native_unit_of_measurement = ELECTRIC_CURRENT_AMPERE
async def async_set_native_value(self, value: float):
self._client.tcp.write(send.set_dc_in_current(
self._client.product, int(value * 1000)))
async def async_update(self):
try:
value = await request(self._client.tcp, send.get_dc_in_current(self._client.product), self._client.dc_in_current_config)
except:
return
self._client.diagnostics["dc_in_current_config"] = value
self._attr_native_value = int(value / 1000)
self._attr_available = True
class GenerateStartEntity(BaseEntity):
_attr_icon = "mdi:engine-outline"
_attr_native_max_value = 30
_attr_native_min_value = 0
_attr_native_step = 1
_attr_native_unit_of_measurement = PERCENTAGE
async def async_set_native_value(self, value: float):
self._client.tcp.write(send.set_generate_start(int(value)))
class GenerateStopEntity(BaseEntity):
_attr_icon = "mdi:engine-off-outline"
_attr_native_max_value = 100
_attr_native_min_value = 50
_attr_native_step = 1
_attr_native_unit_of_measurement = PERCENTAGE
async def async_set_native_value(self, value: float):
self._client.tcp.write(send.set_generate_stop(int(value)))
class LcdBrightnessEntity(BaseEntity):
_attr_icon = "mdi:brightness-6"
_attr_native_max_value = 100
_attr_native_min_value = 0
_attr_native_step = 1
_attr_native_unit_of_measurement = PERCENTAGE
def _on_updated(self, data: dict[str, Any]):
self._attr_native_value = data[self._key] & 0x7F
async def async_set_native_value(self, value: float):
self._client.tcp.write(send.set_lcd(
self._client.product, light=int(value)))
class MaxLevelEntity(BaseEntity):
_attr_icon = "mdi:battery-arrow-up"
_attr_native_max_value = 100
_attr_native_min_value = 30
_attr_native_step = 1
_attr_native_unit_of_measurement = PERCENTAGE
async def async_set_native_value(self, value: float):
self._client.tcp.write(send.set_level_max(
self._client.product, int(value)))
class MinLevelEntity(BaseEntity):
_attr_icon = "mdi:battery-arrow-down-outline"
_attr_native_max_value = 30
_attr_native_min_value = 0
_attr_native_step = 1
_attr_native_unit_of_measurement = PERCENTAGE
async def async_set_native_value(self, value: float):
self._client.tcp.write(send.set_level_min(int(value)))
+196
View File
@@ -0,0 +1,196 @@
from typing import Any
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import FREQUENCY_HERTZ
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import (DOMAIN, EcoFlowConfigEntity, EcoFlowEntity, HassioEcoFlowClient,
request)
from .ecoflow import is_delta, is_power_station, is_river, send
_AC_OPTIONS = {
"Never": 0,
"2hour": 120,
"4hour": 240,
"6hour": 360,
"12hour": 720,
"24hour": 1440,
}
_FREQS = {
"50Hz": 1,
"60Hz": 2,
}
_DC_IMPUTS = {
"Auto": 0,
"Solar": 1,
"Car": 2,
}
_DC_ICONS = {
"Auto": None,
"MPPT": "mdi:solar-power",
"DC": "mdi:current-dc",
}
_LCD_OPTIONS = {
"Never": 0,
"10sec": 10,
"30sec": 30,
"1min": 60,
"5min": 300,
"30min": 1800,
}
_STANDBY_OPTIONS = {
"Never": 0,
"30min": 30,
"1hour": 60,
"2hour": 120,
"6hour": 360,
"12hour": 720,
}
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id]
entities = []
if is_power_station(client.product):
entities.extend([
AcTimeoutEntity(client, client.inverter,
"ac_out_timeout", "AC timeout"),
FreqEntity(client, client.inverter,
"ac_out_freq_config", "AC frequency"),
StandbyTimeoutEntity(
client, client.pd, "standby_timeout", "Unit timeout"),
])
if is_delta(client.product):
entities.extend([
LcdTimeoutPushEntity(client, client.pd,
"lcd_timeout", "Screen timeout"),
])
if is_river(client.product):
entities.extend([
DcInTypeEntity(client),
LcdTimeoutPollEntity(client, "lcd_timeout", "Screen timeout"),
])
async_add_entities(entities)
class AcTimeoutEntity(SelectEntity, EcoFlowEntity):
_attr_current_option = None
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:timer-settings"
_attr_options = list(_AC_OPTIONS.keys())
async def async_select_option(self, option: str):
self._client.tcp.write(send.set_ac_timeout(_AC_OPTIONS[option]))
def _on_updated(self, data: dict[str, Any]):
value = data[self._key]
self._attr_current_option = next(
(i for i in _AC_OPTIONS if _AC_OPTIONS[i] == value), None)
class DcInTypeEntity(SelectEntity, EcoFlowConfigEntity):
_attr_current_option = None
_attr_options = list(_DC_IMPUTS.keys())
def __init__(self, client: HassioEcoFlowClient):
super().__init__(client, "dc_in_type_config", "DC mode")
self._req = send.get_dc_in_type(client.product)
@property
def icon(self):
return _DC_ICONS.get(self.current_option, None)
async def async_select_option(self, option: str):
self._client.tcp.write(send.set_dc_in_type(
self._client.product, _DC_IMPUTS[option]))
async def async_update(self):
try:
value = await request(self._client.tcp, self._req, self._client.dc_in_type)
except:
return
self._client.diagnostics["dc_in_type"] = value
self._attr_current_option = next(
(i for i in _DC_IMPUTS if _DC_IMPUTS[i] == value), None)
self._attr_available = True
class FreqEntity(SelectEntity, EcoFlowEntity):
_attr_current_option = None
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:sine-wave"
_attr_options = list(_FREQS.keys())
_attr_unit_of_measurement = FREQUENCY_HERTZ
async def async_select_option(self, option: str):
self._client.tcp.write(send.set_ac_out(
self._client.product, freq=_FREQS[option]))
def _on_updated(self, data: dict[str, Any]):
value = data[self._key]
self._attr_current_option = next(
(i for i in _FREQS if _FREQS[i] == value), None)
class LcdTimeoutPollEntity(SelectEntity, EcoFlowConfigEntity):
_attr_current_option = None
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:timer-settings"
_attr_options = list(_LCD_OPTIONS.keys())
_req = send.get_lcd()
async def async_select_option(self, option: str):
self._client.tcp.write(send.set_lcd(
self._client.product, time=_LCD_OPTIONS[option]))
async def async_update(self):
try:
value = await request(self._client.tcp, self._req, self._client.lcd_timeout)
except:
return
self._client.diagnostics["lcd_timeout"] = value
self._attr_current_option = next(
(i for i in _LCD_OPTIONS if _LCD_OPTIONS[i] == value), None)
self._attr_available = True
class LcdTimeoutPushEntity(SelectEntity, EcoFlowEntity):
_attr_current_option = None
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:timer-settings"
_attr_options = list(_LCD_OPTIONS.keys())
async def async_select_option(self, option: str):
self._client.tcp.write(send.set_lcd(
self._client.product, time=_LCD_OPTIONS[option]))
def _on_updated(self, data: dict[str, Any]):
value = data[self._key]
self._attr_current_option = next(
(i for i in _LCD_OPTIONS if _LCD_OPTIONS[i] == value), None)
class StandbyTimeoutEntity(SelectEntity, EcoFlowEntity):
_attr_current_option = None
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:timer-settings"
_attr_options = list(_STANDBY_OPTIONS.keys())
async def async_select_option(self, option: str):
self._client.tcp.write(
send.set_standby_timeout(_STANDBY_OPTIONS[option]))
def _on_updated(self, data: dict[str, Any]):
value = data[self._key]
self._attr_current_option = next(
(i for i in _STANDBY_OPTIONS if _STANDBY_OPTIONS[i] == value), None)
+301
View File
@@ -0,0 +1,301 @@
from datetime import timedelta
from typing import Any, Optional, Union
import reactivex.operators as ops
from homeassistant.components.sensor import (SensorDeviceClass, SensorEntity,
SensorStateClass)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (ELECTRIC_CURRENT_AMPERE,
ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR,
FREQUENCY_HERTZ, PERCENTAGE, POWER_WATT,
TEMP_CELSIUS)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
from reactivex import Observable
from . import DOMAIN, EcoFlowEntity, HassioEcoFlowClient, select_bms
from .ecoflow import (is_delta, is_delta_mini, is_delta_pro, is_power_station,
is_river)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id]
entities = []
if is_power_station(client.product):
entities.extend([
CurrentEntity(client, client.inverter,
"ac_in_current", "AC input current"),
CurrentEntity(client, client.inverter,
"ac_out_current", "AC output current"),
EnergyEntity(client, client.pd, "mppt_in_energy",
"MPPT input energy"),
EnergySumEntity(client, "in_energy", [
"ac", "car", "mppt"], "Total input energy"),
EnergySumEntity(client, "out_energy", [
"ac", "car"], "Total output energy"),
FanEntity(client, client.inverter, "fan_state", "Fan"),
FrequencyEntity(client, client.inverter,
"ac_in_freq", "AC input frequency"),
FrequencyEntity(client, client.inverter,
"ac_out_freq", "AC output frequency"),
RemainEntity(client, client.pd, "remain_display", "Remain"),
LevelEntity(client, client.pd, "battery_level",
"Battery"),
VoltageEntity(client, client.inverter,
"ac_in_voltage", "AC input voltage"),
VoltageEntity(client, client.inverter,
"ac_out_voltage", "AC output voltage"),
WattsEntity(client, client.pd, "in_power", "Total input"),
WattsEntity(client, client.pd, "out_power", "Total output"),
WattsEntity(client, client.inverter,
"ac_consumption", "AC output + loss", real=True),
WattsEntity(client, client.inverter, "ac_out_power",
"AC output", real=False),
WattsEntity(client, client.pd, "usb_out1_power",
"USB-A left output"),
WattsEntity(client, client.pd, "usb_out2_power",
"USB-A right output"),
])
if is_delta(client.product):
bms = (
client.bms.pipe(select_bms(0), ops.share()),
client.bms.pipe(select_bms(1), ops.share()),
client.bms.pipe(select_bms(2), ops.share()),
)
entities.extend([
CurrentEntity(client, client.mppt, "dc_in_current",
"DC input current"),
CyclesEntity(
client, bms[0], "battery_cycles", "Main battery cycles", 0),
RemainEntity(client, client.ems,
"battery_remain_charge", "Remain charge"),
RemainEntity(client, client.ems,
"battery_remain_discharge", "Remain discharge"),
SingleLevelEntity(
client, bms[0], "battery_level_f32", "Main battery", 0),
TempEntity(client, client.inverter, "ac_out_temp",
"AC temperature"),
TempEntity(client, bms[0], "battery_temp",
"Main battery temperature", 0),
TempEntity(client, client.mppt, "dc_in_temp",
"DC input temperature"),
TempEntity(client, client.mppt, "dc24_temp",
"DC output temperature"),
TempEntity(client, client.pd, "typec_out1_temp",
"USB-C left temperature"),
TempEntity(client, client.pd, "typec_out2_temp",
"USB-C right temperature"),
VoltageEntity(client, client.mppt, "dc_in_voltage",
"DC input voltage"),
WattsEntity(client, client.inverter,
"ac_in_power", "AC input"),
WattsEntity(client, client.mppt, "dc_in_power",
"DC input", real=True),
WattsEntity(client, client.mppt,
"car_consumption", "Car output + loss", real=True),
WattsEntity(client, client.mppt,
"car_out_power", "Car output"),
])
if is_delta_mini(client.product):
entities.extend([
WattsEntity(client, client.pd,
"usbqc_out1_power", "USB-Fast output"),
WattsEntity(client, client.pd,
"typec_out1_power", "USB-C output"),
])
else:
entities.extend([
CyclesEntity(
client, bms[1], "battery_cycles", "Extra1 battery cycles", 1),
CyclesEntity(
client, bms[2], "battery_cycles", "Extra2 battery cycles", 2),
SingleLevelEntity(
client, bms[1], "battery_level_f32", "Extra1 battery", 1),
SingleLevelEntity(
client, bms[2], "battery_level_f32", "Extra2 battery", 2),
TempEntity(client, bms[1], "battery_temp",
"Extra1 battery temperature", 1),
TempEntity(client, bms[2], "battery_temp",
"Extra2 battery temperature", 2),
WattsEntity(client, client.pd, "usbqc_out1_power",
"USB-Fast left output"),
WattsEntity(client, client.pd, "usbqc_out2_power",
"USB-Fast right output"),
WattsEntity(client, client.pd, "typec_out1_power",
"USB-C left output"),
WattsEntity(client, client.pd, "typec_out2_power",
"USB-C right output"),
])
if is_delta_pro(client.product):
entities.extend([
WattsEntity(client, client.mppt,
"anderson_out_power", "Anderson output"),
])
if is_river(client.product):
extra = client.bms.pipe(select_bms(1), ops.share())
entities.extend([
CurrentEntity(client, client.inverter, "dc_in_current",
"DC input current"),
CyclesEntity(client, client.ems, "battery_cycles",
"Main battery cycles"),
CyclesEntity(client, extra, "battery_cycles",
"Extra battery cycles", 1),
SingleLevelEntity(client, client.ems, "battery_main_level",
"Main battery"),
SingleLevelEntity(
client, extra, "battery_level", "Extra battery", 1),
TempEntity(client, client.inverter, "ac_in_temp",
"AC input temperature"),
TempEntity(client, client.inverter, "ac_out_temp",
"AC output temperature"),
TempEntity(client, client.ems, "battery_main_temp",
"Main battery temperature"),
TempEntity(client, extra, "battery_temp",
"Extra battery temperature", 1),
TempEntity(client, client.pd, "car_out_temp",
"DC output temperature"),
TempEntity(client, client.pd, "typec_out1_temp",
"USB-C temperature"),
VoltageEntity(client, client.inverter, "dc_in_voltage",
"DC input voltage"),
WattsEntity(client, client.pd, "car_out_power", "Car output"),
WattsEntity(client, client.pd, "light_power", "Light output"),
WattsEntity(client, client.pd, "usbqc_out1_power",
"USB-Fast output"),
WattsEntity(client, client.pd, "typec_out1_power",
"USB-C output"),
])
async_add_entities(entities)
class BaseEntity(SensorEntity, EcoFlowEntity):
def _on_updated(self, data: dict[str, Any]):
self._attr_native_value = data[self._key]
class CurrentEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.CURRENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = ELECTRIC_CURRENT_AMPERE
_attr_state_class = SensorStateClass.MEASUREMENT
class CyclesEntity(BaseEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:battery-heart-variant"
_attr_state_class = SensorStateClass.TOTAL_INCREASING
class EnergyEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.ENERGY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = ENERGY_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
class EnergySumEntity(EnergyEntity):
def __init__(self, client: HassioEcoFlowClient, key: str, keys: list[str], name: str):
super().__init__(client, client.pd, key, name)
self._suffix_len = len(key) + 1
self._keys = [f"{x}_{key}" for x in keys]
def _on_updated(self, data: dict[str, Any]):
values = {key[:-self._suffix_len]: data[key]
for key in data if key in self._keys}
self._attr_extra_state_attributes = values
self._attr_native_value = sum(values.values())
class FanEntity(BaseEntity):
_attr_state_class = SensorStateClass.MEASUREMENT
@property
def icon(self):
value = self.native_value
if value is None or self.native_value <= 0:
return "mdi:fan-off"
return "mdi:fan"
class FrequencyEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.FREQUENCY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = FREQUENCY_HERTZ
_attr_state_class = SensorStateClass.MEASUREMENT
class LevelEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, client: HassioEcoFlowClient, src: Observable[dict[str, Any]], key: str, name: str, bms_id: Optional[int] = None):
super().__init__(client, src, key, name, bms_id)
self._attr_extra_state_attributes = {}
class RemainEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_entity_registry_enabled_default = False
def _on_updated(self, data: dict[str, Any]):
value: timedelta = data[self._key]
if value.total_seconds() == 8639940:
self._attr_native_value = None
else:
self._attr_native_value = utcnow() + value
class SingleLevelEntity(LevelEntity):
def _on_updated(self, data: dict[str, Any]):
super()._on_updated(data)
if "battery_capacity_remain" in data:
self._attr_extra_state_attributes["capacity_remain"] = data["battery_capacity_remain"]
if "battery_capacity_full" in data:
self._attr_extra_state_attributes["capacity_full"] = data["battery_capacity_full"]
if "battery_capacity_design" in data:
self._attr_extra_state_attributes["capacity_design"] = data["battery_capacity_design"]
class TempEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = TEMP_CELSIUS
_attr_state_class = SensorStateClass.MEASUREMENT
class VoltageEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.VOLTAGE
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT
_attr_state_class = SensorStateClass.MEASUREMENT
class WattsEntity(BaseEntity):
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = POWER_WATT
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, client: HassioEcoFlowClient, src: Observable[dict[str, Any]], key: str, name: str, real: Union[bool, int] = False):
super().__init__(client, src, key, name)
if key.endswith("_consumption"):
self._key = key[:-11] + "out_power"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._real = real
def _on_updated(self, data: dict[str, Any]):
key = self._key[:-5]
if self._real is not False and f"{key}current" in data and f"{key}voltage" in data:
self._attr_native_value = (
data[f"{key}current"] * data[f"{key}voltage"])
if self._real is not True:
self._attr_native_value = round(
self._attr_native_value, self._real)
if self._real == 0:
self._attr_native_value = int(self._attr_native_value)
else:
super()._on_updated(data)
+184
View File
@@ -0,0 +1,184 @@
from typing import Any
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, EcoFlowEntity, HassioEcoFlowClient, select_bms
from .ecoflow import is_delta, is_power_station, is_river, is_river_mini, send
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id]
entities = []
if is_power_station(client.product):
entities.extend([
AcEntity(client, client.inverter, "ac_out_state", "AC output"),
BeepEntity(client, client.pd, "beep", "Beep"),
])
if is_delta(client.product):
entities.extend([
AcPauseEntity(client, client.inverter,
"ac_in_pause", "AC charge"),
DcEntity(client, client.mppt, "car_out_state", "DC output"),
LcdAutoEntity(client, client.pd, "lcd_brightness",
"Screen brightness auto"),
])
if is_river(client.product):
entities.extend([
AcSlowChargeEntity(client, client.inverter,
"ac_in_slow", "AC slow charging"),
DcEntity(client, client.pd, "car_out_state", "DC output"),
FanAutoEntity(client, client.inverter,
"fan_config", "Auto fan speed"),
])
if client.product == 5: # RIVER Max
entities.extend([
AmbientSyncEntity(client, client.bms.pipe(
select_bms(1)), "ambient_mode", "Ambient light sync screen", 1)
])
if not is_river_mini(client.product):
entities.extend([
XBoostEntity(client, client.inverter,
"ac_out_xboost", "AC X-Boost"),
])
async_add_entities(entities)
class SimpleEntity(SwitchEntity, EcoFlowEntity):
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = bool(data[self._key])
class AcEntity(SimpleEntity):
_attr_device_class = SwitchDeviceClass.OUTLET
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_out(self._client.product, False))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_out(self._client.product, True))
class AcPauseEntity(SimpleEntity):
_attr_entity_category = EntityCategory.CONFIG
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = not bool(data[self._key])
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_in_limit(pause=True))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_in_limit(pause=False))
class AcSlowChargeEntity(SimpleEntity):
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:car-speed-limiter"
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_in_slow(False))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_in_slow(True))
class AmbientSyncEntity(SimpleEntity):
_attr_entity_category = EntityCategory.CONFIG
@property
def icon(self):
return "mdi:sync-off" if self.is_on is False else "mdi:sync"
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_ambient(2))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_ambient(1))
def _on_updated(self, data: dict[str, Any]):
if data[self._key] == 1:
self._attr_is_on = True
elif data[self._key] == 2:
self._attr_is_on = False
else:
self._attr_is_on = None
class BeepEntity(SimpleEntity):
_attr_entity_category = EntityCategory.CONFIG
@property
def icon(self):
return "mdi:volume-source" if self.is_on else "mdi:volume-mute"
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = not bool(data[self._key])
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_beep(False))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_beep(True))
class DcEntity(SimpleEntity):
_attr_device_class = SwitchDeviceClass.OUTLET
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_dc_out(self._client.product, False))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_dc_out(self._client.product, True))
class FanAutoEntity(SimpleEntity):
_attr_entity_category = EntityCategory.CONFIG
@property
def icon(self):
return "mdi:fan-auto" if self.is_on else "mdi:fan-chevron-up"
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_fan_auto(self._client.product, False))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_fan_auto(self._client.product, True))
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = data[self._key] == 1
class LcdAutoEntity(SimpleEntity):
_attr_entity_category = EntityCategory.CONFIG
_attr_icon = "mdi:brightness-auto"
_brightness = 0
def _on_updated(self, data: dict[str, Any]):
self._attr_is_on = bool(data[self._key] & 0x80)
self._brightness = data[self._key] & 0x7F
async def async_turn_off(self, **kwargs: Any):
value = self._brightness
self._client.tcp.write(send.set_lcd(self._client.product, light=value))
async def async_turn_on(self, **kwargs: Any):
value = self._brightness | 0x80
self._client.tcp.write(send.set_lcd(self._client.product, light=value))
class XBoostEntity(SimpleEntity):
_attr_entity_category = EntityCategory.CONFIG
async def async_turn_off(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_out(
self._client.product, xboost=False))
async def async_turn_on(self, **kwargs: Any):
self._client.tcp.write(send.set_ac_out(
self._client.product, xboost=True))
@@ -0,0 +1,18 @@
{
"title": "EcoFlow",
"config": {
"abort": {
"product_unsupported": "Sorry, This product is not supported now."
},
"error": {
"timeout": "Connection timeouted"
},
"step": {
"user": {
"data": {
"host": "Hostname or IP-address"
}
}
}
}
}
@@ -0,0 +1,18 @@
{
"title": "EcoFlow",
"config": {
"abort": {
"product_unsupported": "現時点では、この製品はサポートされていません。"
},
"error": {
"timeout": "接続がタイムアウトしました"
},
"step": {
"user": {
"data": {
"host": "ホスト名またはIPアドレス"
}
}
}
}
}
@@ -38,6 +38,7 @@ from .const import (
ATTR_CLIENT, ATTR_CLIENT,
ATTR_CONFIG, ATTR_CONFIG,
ATTR_COORDINATOR, ATTR_COORDINATOR,
ATTR_WS_EVENT_PROXY,
ATTRIBUTE_LABELS, ATTRIBUTE_LABELS,
CONF_CAMERA_STATIC_IMAGE_HEIGHT, CONF_CAMERA_STATIC_IMAGE_HEIGHT,
DOMAIN, DOMAIN,
@@ -52,6 +53,7 @@ from .const import (
) )
from .views import async_setup as views_async_setup from .views import async_setup as views_async_setup
from .ws_api import async_setup as ws_api_async_setup from .ws_api import async_setup as ws_api_async_setup
from .ws_event_proxy import WSEventProxy
SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL = timedelta(seconds=5)
@@ -215,11 +217,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
model = f"{(await async_get_integration(hass, DOMAIN)).version}/{server_version}" model = f"{(await async_get_integration(hass, DOMAIN)).version}/{server_version}"
ws_event_proxy = WSEventProxy(config["mqtt"]["topic_prefix"])
entry.async_on_unload(lambda: ws_event_proxy.unsubscribe_all(hass))
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
ATTR_COORDINATOR: coordinator, ATTR_COORDINATOR: coordinator,
ATTR_CLIENT: client, ATTR_CLIENT: client,
ATTR_CONFIG: config, ATTR_CONFIG: config,
ATTR_MODEL: model, ATTR_MODEL: model,
ATTR_WS_EVENT_PROXY: ws_event_proxy,
} }
# Remove old devices associated with cameras that have since been removed # Remove old devices associated with cameras that have since been removed
@@ -38,6 +38,7 @@ ATTR_PLAYBACK_FACTOR = "playback_factor"
ATTR_PTZ_ACTION = "action" ATTR_PTZ_ACTION = "action"
ATTR_PTZ_ARGUMENT = "argument" ATTR_PTZ_ARGUMENT = "argument"
ATTR_START_TIME = "start_time" ATTR_START_TIME = "start_time"
ATTR_WS_EVENT_PROXY = "ws_event_proxy"
# Frigate Attribute Labels # Frigate Attribute Labels
# These are labels that are not individually tracked as they are # These are labels that are not individually tracked as they are
@@ -14,5 +14,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"issue_tracker": "https://github.com/blakeblackshear/frigate-hass-integration/issues", "issue_tracker": "https://github.com/blakeblackshear/frigate-hass-integration/issues",
"requirements": ["pytz"], "requirements": ["pytz"],
"version": "5.3.0" "version": "5.4.0"
} }
+72 -2
View File
@@ -6,7 +6,12 @@ import logging
import voluptuous as vol import voluptuous as vol
from custom_components.frigate.api import FrigateApiClient, FrigateApiClientError from custom_components.frigate.api import FrigateApiClient, FrigateApiClientError
from custom_components.frigate.views import get_client_for_frigate_instance_id from custom_components.frigate.const import ATTR_WS_EVENT_PROXY, DOMAIN
from custom_components.frigate.views import (
get_client_for_frigate_instance_id,
get_config_entry_for_frigate_instance_id,
)
from custom_components.frigate.ws_event_proxy import WSEventProxy
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -21,6 +26,8 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, ws_get_events) websocket_api.async_register_command(hass, ws_get_events)
websocket_api.async_register_command(hass, ws_get_events_summary) websocket_api.async_register_command(hass, ws_get_events_summary)
websocket_api.async_register_command(hass, ws_get_ptz_info) websocket_api.async_register_command(hass, ws_get_ptz_info)
websocket_api.async_register_command(hass, ws_subscribe_events)
websocket_api.async_register_command(hass, ws_unsubscribe_events)
def _get_client_or_send_error( def _get_client_or_send_error(
@@ -157,7 +164,6 @@ async def ws_get_recordings_summary(
vol.Optional("limit"): int, vol.Optional("limit"): int,
vol.Optional("has_clip"): bool, vol.Optional("has_clip"): bool,
vol.Optional("has_snapshot"): bool, vol.Optional("has_snapshot"): bool,
vol.Optional("has_snapshot"): bool,
vol.Optional("favorites"): bool, vol.Optional("favorites"): bool,
} }
) # type: ignore[misc] ) # type: ignore[misc]
@@ -237,6 +243,70 @@ async def ws_get_events_summary(
) )
@websocket_api.websocket_command(
{
vol.Required("type"): "frigate/events/subscribe",
vol.Required("instance_id"): str,
}
) # type: ignore[misc]
@websocket_api.async_response # type: ignore[misc]
async def ws_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict,
) -> None:
"""Subscribe to events."""
entry = get_config_entry_for_frigate_instance_id(hass, msg["instance_id"])
if not entry:
connection.send_error(
msg["id"],
"not_found",
f"API error whilst subscribing to events for unknown Frigate instance "
f"{msg['instance_id']}",
)
return
event_proxy: WSEventProxy = hass.data[DOMAIN][entry.entry_id][ATTR_WS_EVENT_PROXY]
connection.send_result(
msg["id"], await event_proxy.subscribe(hass, msg["id"], connection)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "frigate/events/unsubscribe",
vol.Required("instance_id"): str,
vol.Required("subscription_id"): int,
}
) # type: ignore[misc]
@websocket_api.async_response # type: ignore[misc]
async def ws_unsubscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict,
) -> None:
"""Unsubscribe from events."""
entry = get_config_entry_for_frigate_instance_id(hass, msg["instance_id"])
if not entry:
connection.send_error(
msg["id"],
"not_found",
f"API error whilst unsubscribing to events for unknown Frigate instance "
f"{msg['instance_id']}",
)
return
event_proxy: WSEventProxy = hass.data[DOMAIN][entry.entry_id][ATTR_WS_EVENT_PROXY]
if event_proxy.unsubscribe(hass, msg["subscription_id"]):
connection.send_result(msg["id"])
else:
connection.send_error(
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Subscription not found."
)
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required("type"): "frigate/ptz/info", vol.Required("type"): "frigate/ptz/info",
@@ -0,0 +1,95 @@
"""Frigate event proxy."""
from __future__ import annotations
import logging
from homeassistant.components import websocket_api
from homeassistant.components.mqtt.models import ReceiveMessage
from homeassistant.components.mqtt.subscription import (
async_prepare_subscribe_topics,
async_subscribe_topics,
async_unsubscribe_topics,
)
from homeassistant.components.websocket_api import messages
from homeassistant.core import HomeAssistant
_LOGGER: logging.Logger = logging.getLogger(__name__)
class WSEventProxy:
"""Frigate event MQTT to WS proxy.
This class subscribes to the MQTT events topic for a given Frigate topic and
forwards the messages to a list of subscribers. MQTT payload is directly
passed to subscribers to avoid JSON serialization/deserialization overhead
within HA.
"""
def __init__(self, topic_prefix: str) -> None:
self._subscriptions: dict[int, websocket_api.ActiveConnection] = {}
self._topics = {
"events": {
"topic": f"{topic_prefix}/events",
"msg_callback": lambda msg: self._receive_message(msg),
"qos": 0,
}
}
self._sub_state = None
async def subscribe(
self,
hass: HomeAssistant,
subscription_id: int,
connection: websocket_api.ActiveConnection,
) -> int:
"""Subscribe to events."""
if self._sub_state is None:
self._sub_state = async_prepare_subscribe_topics(
hass, self._sub_state, self._topics
)
await async_subscribe_topics(hass, self._sub_state)
# Add a callback to the websocket to unsubscribe if closed.
connection.subscriptions[subscription_id] = lambda: self._unsubscribe_internal(
hass, subscription_id
)
self._subscriptions[subscription_id] = connection
return subscription_id
def unsubscribe(self, hass: HomeAssistant, subscription_id: int) -> bool:
"""Unsubscribe from events."""
if (
subscription_id in self._subscriptions
and subscription_id in self._subscriptions[subscription_id].subscriptions
):
self._subscriptions[subscription_id].subscriptions.pop(subscription_id)
return self._unsubscribe_internal(hass, subscription_id)
def _unsubscribe_internal(self, hass: HomeAssistant, subscription_id: int) -> bool:
"""Unsubscribe from events.
May be called from the websocket connection close handler. As a result
must not change the size of connection.subscriptions which is iterated
over in that handler.
"""
if subscription_id not in self._subscriptions:
return False
self._subscriptions.pop(subscription_id)
if not self._subscriptions:
async_unsubscribe_topics(hass, self._sub_state)
self._sub_state = None
return True
def unsubscribe_all(self, hass: HomeAssistant) -> None:
"""Unsubscribe all subscribers."""
for subscription_id in list(self._subscriptions.keys()):
self.unsubscribe(hass, subscription_id)
def _receive_message(self, msg: ReceiveMessage) -> None:
"""Handle a new received MQTT message."""
for id, connection in self._subscriptions.items():
connection.send_message(messages.event_message(id, msg.payload))
+46 -111
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)
+159 -233
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])
@@ -355,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
@@ -365,18 +362,25 @@ 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: AwesomeVersion | 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:
"""Return the HACS integration dir.""" """Return the HACS integration dir."""
@@ -401,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:
@@ -420,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."""
@@ -458,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."""
@@ -479,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]],
@@ -520,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
@@ -552,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,
@@ -563,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)
@@ -573,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:
@@ -589,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:
@@ -620,79 +614,64 @@ 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, {})
@@ -728,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. "
@@ -746,7 +726,8 @@ 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:
if not nolog: if not nolog:
self.log.exception("Download failed - %s", exception) self.log.exception("Download failed - %s", exception)
@@ -755,15 +736,24 @@ class HacsBase:
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
) )
@@ -776,12 +766,9 @@ 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:
self.enable_hacs_category(HacsCategory.TEMPLATE)
if ( if (
HacsCategory.PYTHON_SCRIPT in self.hass.config.components HacsCategory.PYTHON_SCRIPT in self.hass.config.components
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT) or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
@@ -795,30 +782,24 @@ class HacsBase:
if self.configuration.appdaemon: if self.configuration.appdaemon:
self.enable_hacs_category(HacsCategory.APPDAEMON) self.enable_hacs_category(HacsCategory.APPDAEMON)
if self.configuration.netdaemon:
if self.repositories.category_downloaded(HacsCategory.NETDAEMON):
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:
@@ -829,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:
@@ -848,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 []
] ]
) )
@@ -858,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
@@ -869,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 (
@@ -904,51 +886,7 @@ class HacsBase:
self.repositories.unregister(repository) self.repositories.unregister(repository)
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {}) self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
self.coordinators[category].async_update_listeners()
async def async_get_category_repositories(self, category: HacsCategory) -> None:
"""Get repositories from category."""
if self.system.disabled:
return
try:
repositories = await self.async_github_get_hacs_default_file(category)
except HacsException:
return
for repo in repositories:
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if self.repositories.is_removed(repo):
continue
if repo in self.common.archived_repositories:
continue
repository = self.repositories.get_by_full_name(repo)
if repository is not None:
self.repositories.mark_default(repository)
if self.status.new and self.configuration.dev:
# Force update for new installations
self.queue.add(repository.common_update())
continue
self.queue.add(
self.async_register_repository(
repository_full_name=repo,
category=category,
default=True,
)
)
async def async_update_all_repositories(self, _=None) -> None:
"""Update all repositories."""
if self.system.disabled:
return
self.log.debug("Starting recurring background task for all repositories")
for repository in self.repositories.list_all:
if repository.data.category in self.common.categories:
self.queue.add(repository.common_update())
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {"action": "reload"})
self.log.debug("Recurring background task for all repositories done")
async def async_check_rate_limit(self, _=None) -> None: async def async_check_rate_limit(self, _=None) -> None:
"""Check rate limit.""" """Check rate limit."""
@@ -960,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")
@@ -1002,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
@@ -1022,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. "
@@ -1051,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")
@@ -1086,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:
@@ -1143,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
@@ -1159,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
+17 -40
View File
@@ -1,4 +1,5 @@
"""Adds config flow for HACS.""" """Adds config flow for HACS."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@@ -23,14 +24,9 @@ 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,
) )
@@ -75,15 +71,9 @@ class HacsFlowHandler(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)
@callback
def async_remove(self):
"""Cleanup."""
if self.activation_task and not self.activation_task.done():
self.activation_task.cancel()
async def async_step_device(self, _user_input): async def async_step_device(self, _user_input):
"""Handle device steps.""" """Handle device steps."""
@@ -97,8 +87,6 @@ class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
with suppress(UnknownFlow): with suppress(UnknownFlow):
await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
self.hass.async_create_task(_progress())
if not self.device: if not self.device:
integration = await async_get_integration(self.hass, DOMAIN) integration = await async_get_integration(self.hass, DOMAIN)
self.device = GitHubDeviceAPI( self.device = GitHubDeviceAPI(
@@ -122,14 +110,16 @@ class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_show_progress_done(next_step_id="could_not_register") return self.async_show_progress_done(next_step_id="could_not_register")
return self.async_show_progress_done(next_step_id="device_done") return self.async_show_progress_done(next_step_id="device_done")
return self.async_show_progress( show_progress_kwargs = {
step_id="device", "step_id": "device",
progress_action="wait_for_device", "progress_action": "wait_for_device",
description_placeholders={ "description_placeholders": {
"url": OAUTH_USER_LOGIN, "url": OAUTH_USER_LOGIN,
"code": self._registration.user_code, "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."""
@@ -152,9 +142,6 @@ class HacsFlowHandler(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,
@@ -176,7 +163,7 @@ class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
"token": self._activation.access_token, "token": self._activation.access_token,
}, },
options={ options={
"experimental": self._user_input.get("experimental", False), "experimental": True,
}, },
) )
@@ -219,10 +206,7 @@ class HacsOptionsFlowHandler(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")
@@ -230,18 +214,11 @@ class HacsOptionsFlowHandler(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))
+2 -1
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"
@@ -0,0 +1,38 @@
"""Coordinator to trigger entity updates."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.update_coordinator import BaseDataUpdateCoordinatorProtocol
class HacsUpdateCoordinator(BaseDataUpdateCoordinatorProtocol):
"""Dispatch updates to update entities."""
def __init__(self) -> None:
"""Initialize."""
self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}
@callback
def async_add_listener(
self, update_callback: CALLBACK_TYPE, context: Any = None
) -> Callable[[], None]:
"""Listen for data updates."""
@callback
def remove_listener() -> None:
"""Remove update listener."""
self._listeners.pop(remove_listener)
self._listeners[remove_listener] = (update_callback, context)
return remove_listener
@callback
def async_update_listeners(self) -> None:
"""Update all registered listeners."""
for update_callback, _ in list(self._listeners.values()):
update_callback()
+44 -3
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."""
+2 -4
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",))
+36 -12
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.
"""
+2 -21
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."""
+22 -40
View File
@@ -1,71 +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,
)
try:
from homeassistant.components.frontend import add_extra_js_url
except ImportError:
def add_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None:
hacs: HacsBase = hass.data.get(DOMAIN)
hacs.log.error("Could not import add_extra_js_url from frontend.")
if "frontend_extra_module_url" not in hass.data:
hass.data["frontend_extra_module_url"] = set()
hass.data["frontend_extra_module_url"].add(url)
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")
) )
add_extra_js_url(hass, f"{URL_BASE}/iconset.js") add_extra_js_url(hass, f"{URL_BASE}/iconset.js")
hacs.frontend_version = (
FE_VERSION if not hacs.configuration.experimental else EXPERIMENTAL_FE_VERSION
)
# 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,
@@ -82,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
@@ -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};
@@ -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);
@@ -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
@@ -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
@@ -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
@@ -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};
@@ -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};
@@ -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);
@@ -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);
@@ -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};
@@ -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
@@ -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};
@@ -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
@@ -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
@@ -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};
@@ -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")}});
@@ -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
@@ -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);
@@ -1 +0,0 @@
var a=[];export{a as default};
File diff suppressed because one or more lines are too long
@@ -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};
@@ -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};
@@ -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};
@@ -1,108 +0,0 @@
import{a as s,H as i,e,t,L as a,N as o,at as r,a0 as n,a1 as l,$ as c,o as h,au as d,ai as p,s as m,d as _,r as v,n as u}from"./main-ad130be7.js";import{c as y}from"./c.4a97632a.js";import"./c.f1291e50.js";import"./c.2ee83bd0.js";import{s as g}from"./c.4266acdb.js";import{f,a as $}from"./c.fe747ba2.js";import{m as b}from"./c.f6611997.js";import{u as x}from"./c.25ed1ae4.js";import"./c.5d3ce9d6.js";import"./c.82e03b89.js";import"./c.743a15a1.js";import"./c.710a50fc.js";import"./c.8e28b461.js";let k=s([u("hacs-update-dialog")],(function(s,i){class u extends i{constructor(...i){super(...i),s(this)}}return{F:u,d:[{kind:"field",decorators:[e()],key:"repository",value:void 0},{kind:"field",decorators:[e({type:Boolean})],key:"_updating",value:()=>!1},{kind:"field",decorators:[e()],key:"_error",value:void 0},{kind:"field",decorators:[e({attribute:!1})],key:"_releaseNotes",value:()=>[]},{kind:"field",decorators:[t()],key:"_repository",value:void 0},{kind:"method",key:"firstUpdated",value:async function(s){a(o(u.prototype),"firstUpdated",this).call(this,s),this._repository=await f(this.hass,this.repository),this._repository&&("commit"!==this._repository.version_or_commit&&(this._releaseNotes=await r(this.hass,String(this._repository.id))),n(this.hass,(s=>this._error=s),l.ERROR))}},{kind:"method",key:"render",value:function(){var s;return this.active&&this._repository?c`
<hacs-dialog
.active=${this.active}
.title=${this.hacs.localize("dialog_update.title")}
.hass=${this.hass}
>
<div class=${h({content:!0,narrow:this.narrow})}>
<p class="message">
${this.hacs.localize("dialog_update.message",{name:this._repository.name})}
</p>
<div class="version-container">
<div class="version-element">
<span class="version-number">${this._repository.installed_version}</span>
<small class="version-text">${this.hacs.localize("dialog_update.downloaded_version")}</small>
</div>
<span class="version-separator">
<ha-svg-icon
.path=${d}
></ha-svg-icon>
</span>
<div class="version-element">
<span class="version-number">${this._repository.available_version}</span>
<small class="version-text">${this.hacs.localize("dialog_update.available_version")}</small>
</div>
</div>
</div>
${this._releaseNotes.length>0?this._releaseNotes.map((s=>c`
<ha-expansion-panel
.header=${s.name&&s.name!==s.tag?`${s.tag}: ${s.name}`:s.tag}
outlined
?expanded=${1===this._releaseNotes.length}
>
${s.body?b.html(s.body,this._repository):this.hacs.localize("dialog_update.no_info")}
</ha-expansion-panel>
`)):""}
${this._repository.can_download?"":c`<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>`}
${"integration"===this._repository.category?c`<p>${this.hacs.localize("dialog_download.restart")}</p>`:""}
${null!==(s=this._error)&&void 0!==s&&s.message?c`<ha-alert alert-type="error" .rtl=${y(this.hass)}>
${this._error.message}
</ha-alert>`:""}
</div>
<mwc-button
slot="primaryaction"
?disabled=${!this._repository.can_download}
@click=${this._updateRepository}
raised
>
${this._updating?c`<ha-circular-progress active size="small"></ha-circular-progress>`:this.hacs.localize("common.update")}
</mwc-button
>
<div class="secondary" slot="secondaryaction">
<hacs-link .url=${this._getChanglogURL()||""}>
<mwc-button>${this.hacs.localize("dialog_update.changelog")}
</mwc-button>
</hacs-link>
<hacs-link .url="https://github.com/${this._repository.full_name}">
<mwc-button>${this.hacs.localize("common.repository")}
</mwc-button>
</hacs-link>
</div>
</hacs-dialog>
`:c``}},{kind:"method",key:"_updateRepository",value:async function(){this._updating=!0,"commit"!==this._repository.version_or_commit?await $(this.hass,String(this._repository.id),this._repository.available_version):await $(this.hass,String(this._repository.id)),"plugin"===this._repository.category&&"storage"===this.hacs.info.lovelace_mode&&await x(this.hass,this._repository,this._repository.available_version),this._updating=!1,this.dispatchEvent(new Event("hacs-dialog-closed",{bubbles:!0,composed:!0})),"plugin"===this._repository.category&&g(this,{title:this.hacs.localize("common.reload"),text:c`${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:()=>{p.location.href=p.location.href}})}},{kind:"method",key:"_getChanglogURL",value:function(){return"commit"===this._repository.version_or_commit?`https://github.com/${this._repository.full_name}/compare/${this._repository.installed_version}...${this._repository.available_version}`:`https://github.com/${this._repository.full_name}/releases`}},{kind:"get",static:!0,key:"styles",value:function(){return[m,_,v`
.content {
width: 360px;
display: contents;
}
ha-expansion-panel {
margin: 8px 0;
}
ha-expansion-panel[expanded] {
padding-bottom: 16px;
}
.secondary {
display: flex;
}
.message {
text-align: center;
margin: 0;
}
.version-container {
margin: 24px 0 12px 0;
width: 360px;
min-width: 100%;
max-width: 100%;
display: flex;
flex-direction: row;
}
.version-element {
display: flex;
flex-direction: column;
flex: 1;
padding: 0 12px;
text-align: center;
}
.version-text {
color: var(--secondary-text-color);
}
.version-number {
font-size: 1.5rem;
margin-bottom: 4px;
}
`]}}]}}),i);export{k as HacsUpdateDialog};
@@ -1,17 +0,0 @@
import{a as e,H as i,e as t,$ as o,P as s,d as a,r as c,n}from"./main-ad130be7.js";import{c as l}from"./c.710a50fc.js";e([n("hacs-dialog")],(function(e,i){return{F:class extends i{constructor(...i){super(...i),e(this)}},d:[{kind:"field",decorators:[t({type:Boolean})],key:"hideActions",value:()=>!1},{kind:"field",decorators:[t({type:Boolean})],key:"scrimClickAction",value:()=>!1},{kind:"field",decorators:[t({type:Boolean})],key:"escapeKeyAction",value:()=>!1},{kind:"field",decorators:[t({type:Boolean})],key:"noClose",value:()=>!1},{kind:"field",decorators:[t({type:Boolean})],key:"maxWidth",value:()=>!1},{kind:"field",decorators:[t()],key:"title",value:void 0},{kind:"method",key:"render",value:function(){return this.active?o`<ha-dialog
?maxWidth=${this.maxWidth}
?open=${this.active}
?scrimClickAction=${this.scrimClickAction}
?escapeKeyAction=${this.escapeKeyAction}
@closed=${this.closeDialog}
?hideActions=${this.hideActions}
.heading=${this.noClose?this.title:l(this.hass,this.title)}
>
<slot></slot>
<slot class="primary" name="primaryaction" slot="primaryAction"></slot>
<slot class="secondary" name="secondaryaction" slot="secondaryAction"></slot>
</ha-dialog>`:o``}},{kind:"method",key:"closeDialog",value:function(){this.active=!1,this.dispatchEvent(new CustomEvent("closed",{bubbles:!0,composed:!0}))}},{kind:"get",static:!0,key:"styles",value:function(){return[s,a,c`
ha-dialog[maxWidth] {
--mdc-dialog-max-width: calc(100vw - 32px);
}
`]}}]}}),i);
File diff suppressed because one or more lines are too long
@@ -1,50 +0,0 @@
import{u as e,v as t,M as c,_ as i,e as r,K as s,i as o,g as d,t as a,w as n,B as h,R as l,y as p,$ as u,j as m,r as b,A as w,a as v,L as k,N as f,n as _}from"./main-ad130be7.js";import{o as y}from"./c.8e28b461.js";var g={CHECKED:"mdc-switch--checked",DISABLED:"mdc-switch--disabled"},C={ARIA_CHECKED_ATTR:"aria-checked",NATIVE_CONTROL_SELECTOR:".mdc-switch__native-control",RIPPLE_SURFACE_SELECTOR:".mdc-switch__thumb-underlay"},x=function(c){function i(e){return c.call(this,t(t({},i.defaultAdapter),e))||this}return e(i,c),Object.defineProperty(i,"strings",{get:function(){return C},enumerable:!1,configurable:!0}),Object.defineProperty(i,"cssClasses",{get:function(){return g},enumerable:!1,configurable:!0}),Object.defineProperty(i,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},setNativeControlChecked:function(){},setNativeControlDisabled:function(){},setNativeControlAttr:function(){}}},enumerable:!1,configurable:!0}),i.prototype.setChecked=function(e){this.adapter.setNativeControlChecked(e),this.updateAriaChecked(e),this.updateCheckedStyling(e)},i.prototype.setDisabled=function(e){this.adapter.setNativeControlDisabled(e),e?this.adapter.addClass(g.DISABLED):this.adapter.removeClass(g.DISABLED)},i.prototype.handleChange=function(e){var t=e.target;this.updateAriaChecked(t.checked),this.updateCheckedStyling(t.checked)},i.prototype.updateCheckedStyling=function(e){e?this.adapter.addClass(g.CHECKED):this.adapter.removeClass(g.CHECKED)},i.prototype.updateAriaChecked=function(e){this.adapter.setNativeControlAttr(C.ARIA_CHECKED_ATTR,""+!!e)},i}(c);class R extends h{constructor(){super(...arguments),this.checked=!1,this.disabled=!1,this.shouldRenderRipple=!1,this.mdcFoundationClass=x,this.rippleHandlers=new l((()=>(this.shouldRenderRipple=!0,this.ripple)))}changeHandler(e){this.mdcFoundation.handleChange(e),this.checked=this.formElement.checked}createAdapter(){return Object.assign(Object.assign({},p(this.mdcRoot)),{setNativeControlChecked:e=>{this.formElement.checked=e},setNativeControlDisabled:e=>{this.formElement.disabled=e},setNativeControlAttr:(e,t)=>{this.formElement.setAttribute(e,t)}})}renderRipple(){return this.shouldRenderRipple?u`
<mwc-ripple
.accent="${this.checked}"
.disabled="${this.disabled}"
unbounded>
</mwc-ripple>`:""}focus(){const e=this.formElement;e&&(this.rippleHandlers.startFocus(),e.focus())}blur(){const e=this.formElement;e&&(this.rippleHandlers.endFocus(),e.blur())}click(){this.formElement&&!this.disabled&&(this.formElement.focus(),this.formElement.click())}firstUpdated(){super.firstUpdated(),this.shadowRoot&&this.mdcRoot.addEventListener("change",(e=>{this.dispatchEvent(new Event("change",e))}))}render(){return u`
<div class="mdc-switch">
<div class="mdc-switch__track"></div>
<div class="mdc-switch__thumb-underlay">
${this.renderRipple()}
<div class="mdc-switch__thumb">
<input
type="checkbox"
id="basic-switch"
class="mdc-switch__native-control"
role="switch"
aria-label="${m(this.ariaLabel)}"
aria-labelledby="${m(this.ariaLabelledBy)}"
@change="${this.changeHandler}"
@focus="${this.handleRippleFocus}"
@blur="${this.handleRippleBlur}"
@mousedown="${this.handleRippleMouseDown}"
@mouseenter="${this.handleRippleMouseEnter}"
@mouseleave="${this.handleRippleMouseLeave}"
@touchstart="${this.handleRippleTouchStart}"
@touchend="${this.handleRippleDeactivate}"
@touchcancel="${this.handleRippleDeactivate}">
</div>
</div>
</div>`}handleRippleMouseDown(e){const t=()=>{window.removeEventListener("mouseup",t),this.handleRippleDeactivate()};window.addEventListener("mouseup",t),this.rippleHandlers.startPress(e)}handleRippleTouchStart(e){this.rippleHandlers.startPress(e)}handleRippleDeactivate(){this.rippleHandlers.endPress()}handleRippleMouseEnter(){this.rippleHandlers.startHover()}handleRippleMouseLeave(){this.rippleHandlers.endHover()}handleRippleFocus(){this.rippleHandlers.startFocus()}handleRippleBlur(){this.rippleHandlers.endFocus()}}i([r({type:Boolean}),y((function(e){this.mdcFoundation.setChecked(e)}))],R.prototype,"checked",void 0),i([r({type:Boolean}),y((function(e){this.mdcFoundation.setDisabled(e)}))],R.prototype,"disabled",void 0),i([s,r({attribute:"aria-label"})],R.prototype,"ariaLabel",void 0),i([s,r({attribute:"aria-labelledby"})],R.prototype,"ariaLabelledBy",void 0),i([o(".mdc-switch")],R.prototype,"mdcRoot",void 0),i([o("input")],R.prototype,"formElement",void 0),i([d("mwc-ripple")],R.prototype,"ripple",void 0),i([a()],R.prototype,"shouldRenderRipple",void 0),i([n({passive:!0})],R.prototype,"handleRippleMouseDown",null),i([n({passive:!0})],R.prototype,"handleRippleTouchStart",null);const E=b`.mdc-switch__thumb-underlay{left:-14px;right:initial;top:-17px;width:48px;height:48px}[dir=rtl] .mdc-switch__thumb-underlay,.mdc-switch__thumb-underlay[dir=rtl]{left:initial;right:-14px}.mdc-switch__native-control{width:64px;height:48px}.mdc-switch{display:inline-block;position:relative;outline:none;user-select:none}.mdc-switch.mdc-switch--checked .mdc-switch__track{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-switch.mdc-switch--checked .mdc-switch__thumb{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786);border-color:#018786;border-color:var(--mdc-theme-secondary, #018786)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb{background-color:#fff;background-color:var(--mdc-theme-surface, #fff);border-color:#fff;border-color:var(--mdc-theme-surface, #fff)}.mdc-switch__native-control{left:0;right:initial;position:absolute;top:0;margin:0;opacity:0;cursor:pointer;pointer-events:auto;transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-switch__native-control,.mdc-switch__native-control[dir=rtl]{left:initial;right:0}.mdc-switch__track{box-sizing:border-box;width:36px;height:14px;border:1px solid transparent;border-radius:7px;opacity:.38;transition:opacity 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb-underlay{display:flex;position:absolute;align-items:center;justify-content:center;transform:translateX(0);transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);box-sizing:border-box;width:20px;height:20px;border:10px solid;border-radius:50%;pointer-events:none;z-index:1}.mdc-switch--checked .mdc-switch__track{opacity:.54}.mdc-switch--checked .mdc-switch__thumb-underlay{transform:translateX(16px)}[dir=rtl] .mdc-switch--checked .mdc-switch__thumb-underlay,.mdc-switch--checked .mdc-switch__thumb-underlay[dir=rtl]{transform:translateX(-16px)}.mdc-switch--checked .mdc-switch__native-control{transform:translateX(-16px)}[dir=rtl] .mdc-switch--checked .mdc-switch__native-control,.mdc-switch--checked .mdc-switch__native-control[dir=rtl]{transform:translateX(16px)}.mdc-switch--disabled{opacity:.38;pointer-events:none}.mdc-switch--disabled .mdc-switch__thumb{border-width:1px}.mdc-switch--disabled .mdc-switch__native-control{cursor:default;pointer-events:none}:host{display:inline-flex;outline:none;-webkit-tap-highlight-color:transparent}`;v([_("ha-switch")],(function(e,t){class c extends t{constructor(...t){super(...t),e(this)}}return{F:c,d:[{kind:"field",decorators:[r({type:Boolean})],key:"haptic",value:()=>!1},{kind:"method",key:"firstUpdated",value:function(){k(f(c.prototype),"firstUpdated",this).call(this),this.addEventListener("change",(()=>{this.haptic&&w(window,"haptic","light")}))}},{kind:"field",static:!0,key:"styles",value:()=>[E,b`
:host {
--mdc-theme-secondary: var(--switch-checked-color);
}
.mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-color: var(--switch-checked-button-color);
border-color: var(--switch-checked-button-color);
}
.mdc-switch.mdc-switch--checked .mdc-switch__track {
background-color: var(--switch-checked-track-color);
border-color: var(--switch-checked-track-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
background-color: var(--switch-unchecked-button-color);
border-color: var(--switch-unchecked-button-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
background-color: var(--switch-unchecked-track-color);
border-color: var(--switch-unchecked-track-color);
}
`]}]}}),R);
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
const r=r=>(s,o)=>{if(s.constructor._observers){if(!s.constructor.hasOwnProperty("_observers")){const r=s.constructor._observers;s.constructor._observers=new Map,r.forEach(((r,o)=>s.constructor._observers.set(o,r)))}}else{s.constructor._observers=new Map;const r=s.updated;s.updated=function(s){r.call(this,s),s.forEach(((r,s)=>{const o=this.constructor._observers.get(s);void 0!==o&&o.call(this,this[s],r)}))}}s.constructor._observers.set(o,r)};export{r as o};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,30 +0,0 @@
import{a as e,h as i,e as t,$ as r,d as o,r as c,n as d}from"./main-ad130be7.js";import"./c.9b92f489.js";e([d("hacs-filter")],(function(e,i){return{F:class extends i{constructor(...i){super(...i),e(this)}},d:[{kind:"field",decorators:[t({attribute:!1})],key:"filters",value:void 0},{kind:"field",decorators:[t({attribute:!1})],key:"hacs",value:void 0},{kind:"method",key:"render",value:function(){var e;return r`
<div class="filter">
${null===(e=this.filters)||void 0===e?void 0:e.map((e=>r`
<ha-formfield
class="checkbox"
.label=${this.hacs.localize(`common.${e.id}`)||e.value}
.id=${e.id}
@click=${this._filterClick}
>
<ha-checkbox .checked=${e.checked||!1}> </ha-checkbox>
</ha-formfield>
`))}
</div>
`}},{kind:"method",key:"_filterClick",value:function(e){const i=e.currentTarget;this.dispatchEvent(new CustomEvent("filter-change",{detail:{id:i.id},bubbles:!0,composed:!0}))}},{kind:"get",static:!0,key:"styles",value:function(){return[o,c`
.filter {
display: flex;
border-bottom: 1px solid var(--divider-color);
align-items: center;
font-size: 16px;
height: 32px;
line-height: 4px;
background-color: var(--sidebar-background-color);
padding: 0 16px;
box-sizing: border-box;
}
.checkbox:not(:first-child) {
margin-left: 20px;
}
`]}}]}}),i);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
import{a as e,H as t,e as i,m as o,$ as s,n as r}from"./main-ad130be7.js";import{m as a}from"./c.f6611997.js";import"./c.82e03b89.js";import"./c.5d3ce9d6.js";import"./c.743a15a1.js";import"./c.710a50fc.js";import"./c.8e28b461.js";let d=e([r("hacs-generic-dialog")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[i({type:Boolean})],key:"markdown",value:()=>!1},{kind:"field",decorators:[i()],key:"repository",value:void 0},{kind:"field",decorators:[i()],key:"header",value:void 0},{kind:"field",decorators:[i()],key:"content",value:void 0},{kind:"field",key:"_getRepository",value:()=>o(((e,t)=>null==e?void 0:e.find((e=>String(e.id)===t))))},{kind:"method",key:"render",value:function(){if(!this.active||!this.repository)return s``;const e=this._getRepository(this.hacs.repositories,this.repository);return s`
<hacs-dialog .active=${this.active} .narrow=${this.narrow} .hass=${this.hass}>
<div slot="header">${this.header||""}</div>
${this.markdown?this.repository?a.html(this.content||"",e):a.html(this.content||""):this.content||""}
</hacs-dialog>
`}}]}}),t);export{d as HacsGenericDialog};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,30 +0,0 @@
import{a as i,H as s,e as t,$ as e,d as a,r as o,ap as r,aq as l,am as n,ar as c,as as h,n as d}from"./main-ad130be7.js";import"./c.82e03b89.js";import"./c.710a50fc.js";import"./c.8e28b461.js";let m=i([d("hacs-removed-dialog")],(function(i,s){return{F:class extends s{constructor(...s){super(...s),i(this)}},d:[{kind:"field",decorators:[t({attribute:!1})],key:"repository",value:void 0},{kind:"field",decorators:[t({type:Boolean})],key:"_updating",value:()=>!1},{kind:"method",key:"render",value:function(){if(!this.active)return e``;const i=this.hacs.removed.find((i=>i.repository===this.repository.full_name));return e`
<hacs-dialog
.active=${this.active}
.hass=${this.hass}
.title=${this.hacs.localize("entry.messages.removed_repository",{repository:this.repository.full_name})}
>
<div class="content">
<div><b>${this.hacs.localize("dialog_removed.name")}:</b> ${this.repository.name}</div>
${i.removal_type?e` <div>
<b>${this.hacs.localize("dialog_removed.type")}:</b> ${i.removal_type}
</div>`:""}
${i.reason?e` <div>
<b>${this.hacs.localize("dialog_removed.reason")}:</b> ${i.reason}
</div>`:""}
${i.link?e` <div>
</b><hacs-link .url=${i.link}>${this.hacs.localize("dialog_removed.link")}</hacs-link>
</div>`:""}
</div>
<mwc-button slot="secondaryaction" @click=${this._ignoreRepository}>
${this.hacs.localize("common.ignore")}
</mwc-button>
<mwc-button class="uninstall" slot="primaryaction" @click=${this._uninstallRepository}
>${this._updating?e`<ha-circular-progress active size="small"></ha-circular-progress>`:this.hacs.localize("common.remove")}</mwc-button
>
</hacs-dialog>
`}},{kind:"get",static:!0,key:"styles",value:function(){return[a,o`
.uninstall {
--mdc-theme-primary: var(--hcv-color-error);
}
`]}},{kind:"method",key:"_lovelaceUrl",value:function(){var i,s;return`/hacsfiles/${null===(i=this.repository)||void 0===i?void 0:i.full_name.split("/")[1]}/${null===(s=this.repository)||void 0===s?void 0:s.file_name}`}},{kind:"method",key:"_ignoreRepository",value:async function(){await r(this.hass,this.repository.full_name);const i=await l(this.hass);this.dispatchEvent(new CustomEvent("update-hacs",{detail:{removed:i},bubbles:!0,composed:!0})),this.dispatchEvent(new Event("hacs-dialog-closed",{bubbles:!0,composed:!0}))}},{kind:"method",key:"_uninstallRepository",value:async function(){if(this._updating=!0,"plugin"===this.repository.category&&this.hacs.info&&"yaml"!==this.hacs.info.lovelace_mode){(await n(this.hass)).filter((i=>i.url.startsWith(this._lovelaceUrl()))).forEach((i=>{c(this.hass,String(i.id))}))}await h(this.hass,String(this.repository.id)),this._updating=!1,this.active=!1}}]}}),s);export{m as HacsRemovedDialog};
@@ -1,90 +0,0 @@
import{a as s,H as o,e as t,t as a,$ as i,Z as e,a0 as r,a1 as c,a2 as d,a3 as h,a4 as n,s as l,d as p,r as m,n as u}from"./main-ad130be7.js";import{c as v}from"./c.4a97632a.js";import"./c.f1291e50.js";import"./c.d262aab0.js";import"./c.3da15c48.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 g=s([u("hacs-custom-repositories-dialog")],(function(s,o){return{F:class extends o{constructor(...o){super(...o),s(this)}},d:[{kind:"field",decorators:[t()],key:"_error",value:void 0},{kind:"field",decorators:[a()],key:"_progress",value:()=>!1},{kind:"field",decorators:[a()],key:"_addRepositoryData",value:()=>({category:void 0,repository:void 0})},{kind:"field",decorators:[a()],key:"_customRepositories",value:void 0},{kind:"method",key:"shouldUpdate",value:function(s){return s.has("narrow")||s.has("active")||s.has("_error")||s.has("_addRepositoryData")||s.has("_customRepositories")||s.has("_progress")}},{kind:"method",key:"render",value:function(){var s,o;if(!this.active)return i``;const t=[{name:"repository",selector:{text:{}}},{name:"category",selector:{select:{mode:"dropdown",options:this.hacs.info.categories.map((s=>({value:s,label:this.hacs.localize(`common.${s}`)})))}}}];return i`
<hacs-dialog
.active=${this.active}
.hass=${this.hass}
.title=${this.hacs.localize("dialog_custom_repositories.title")}
scrimClickAction
escapeKeyAction
maxWidth
>
<div class="content">
<div class="list" ?narrow=${this.narrow}>
${null!==(s=this._error)&&void 0!==s&&s.message?i`<ha-alert alert-type="error" .rtl=${v(this.hass)}>
${this._error.message}
</ha-alert>`:""}
${null===(o=this._customRepositories)||void 0===o?void 0:o.filter((s=>this.hacs.info.categories.includes(s.category))).map((s=>i`<a
href="/hacs/repository/${s.id}"
@click=${()=>this.active=!1}
>
<ha-settings-row>
<span slot="heading">${s.name}</span>
<span slot="description">${s.full_name} (${s.category})</span>
<mwc-icon-button
@click=${o=>{o.preventDefault(),this._removeRepository(String(s.id))}}
>
<ha-svg-icon class="delete" .path=${e}></ha-svg-icon>
</mwc-icon-button>
</ha-settings-row>
</a>`))}
</div>
<ha-form
?narrow=${this.narrow}
.data=${this._addRepositoryData}
.schema=${t}
.computeLabel=${s=>"category"===s.name?this.hacs.localize("dialog_custom_repositories.category"):this.hacs.localize("common.repository")}
@value-changed=${this._valueChanged}
>
</ha-form>
</div>
<mwc-button
slot="primaryaction"
raised
.disabled=${void 0===this._addRepositoryData.category||void 0===this._addRepositoryData.repository}
@click=${this._addRepository}
>
${this._progress?i`<ha-circular-progress active size="small"></ha-circular-progress>`:this.hacs.localize("common.add")}
</mwc-button>
</hacs-dialog>
`}},{kind:"method",key:"firstUpdated",value:function(){var s;r(this.hass,(s=>this._error=s),c.ERROR),this._customRepositories=null===(s=this.hacs.repositories)||void 0===s?void 0:s.filter((s=>s.custom))}},{kind:"method",key:"_valueChanged",value:function(s){this._addRepositoryData=s.detail.value}},{kind:"method",key:"_addRepository",value:async function(){if(this._error=void 0,this._progress=!0,!this._addRepositoryData.category)return void(this._error={message:this.hacs.localize("dialog_custom_repositories.no_category")});if(!this._addRepositoryData.repository)return void(this._error={message:this.hacs.localize("dialog_custom_repositories.no_repository")});await d(this.hass,this._addRepositoryData.repository,this._addRepositoryData.category);const s=await h(this.hass);this.dispatchEvent(new CustomEvent("update-hacs",{detail:{repositories:s},bubbles:!0,composed:!0})),this._customRepositories=s.filter((s=>s.custom)),this._progress=!1}},{kind:"method",key:"_removeRepository",value:async function(s){this._error=void 0,await n(this.hass,s);const o=await h(this.hass);this.dispatchEvent(new CustomEvent("update-hacs",{detail:{repositories:o},bubbles:!0,composed:!0})),this._customRepositories=o.filter((s=>s.custom))}},{kind:"get",static:!0,key:"styles",value:function(){return[l,p,m`
.list {
position: relative;
max-height: calc(100vh - 500px);
overflow: auto;
}
a {
all: unset;
}
ha-form {
display: block;
padding: 25px 0;
}
ha-form[narrow] {
background-color: var(--card-background-color);
bottom: 0;
position: absolute;
width: calc(100% - 48px);
}
ha-svg-icon {
--mdc-icon-size: 36px;
}
ha-svg-icon:not(.delete) {
margin-right: 4px;
}
ha-settings-row {
cursor: pointer;
padding: 0;
}
.list[narrow] > ha-settings-row:last-of-type {
margin-bottom: 162px;
}
.delete {
color: var(--hcv-color-error);
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.list {
max-height: calc(100vh - 162px);
}
}
`]}}]}}),o);export{g as HacsCustomRepositoriesDialog};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +0,0 @@
import{a as e,H as i,e as t,$ as s,n as o}from"./main-ad130be7.js";import"./c.82e03b89.js";import"./c.710a50fc.js";import"./c.8e28b461.js";let c=e([o("hacs-progress-dialog")],(function(e,i){return{F:class extends i{constructor(...i){super(...i),e(this)}},d:[{kind:"field",decorators:[t()],key:"title",value:void 0},{kind:"field",decorators:[t()],key:"content",value:void 0},{kind:"field",decorators:[t()],key:"confirmText",value:void 0},{kind:"field",decorators:[t()],key:"confirm",value:void 0},{kind:"field",decorators:[t({type:Boolean})],key:"_inProgress",value:()=>!1},{kind:"method",key:"shouldUpdate",value:function(e){return e.has("active")||e.has("title")||e.has("content")||e.has("confirmText")||e.has("confirm")||e.has("_inProgress")}},{kind:"method",key:"render",value:function(){return this.active?s`
<hacs-dialog .active=${this.active} .hass=${this.hass} title=${this.title||""}>
<div class="content">
${this.content||""}
</div>
<mwc-button slot="secondaryaction" ?disabled=${this._inProgress} @click=${this._close}>
${this.hacs.localize("common.cancel")}
</mwc-button>
<mwc-button slot="primaryaction" @click=${this._confirmed}>
${this._inProgress?s`<ha-circular-progress active size="small"></ha-circular-progress>`:this.confirmText||this.hacs.localize("common.yes")}</mwc-button
>
</mwc-button>
</hacs-dialog>
`:s``}},{kind:"method",key:"_confirmed",value:async function(){this._inProgress=!0,await this.confirm(),this._inProgress=!1,this._close()}},{kind:"method",key:"_close",value:function(){this.active=!1,this.dispatchEvent(new Event("hacs-dialog-closed",{bubbles:!0,composed:!0}))}}]}}),i);export{c as HacsProgressDialog};
@@ -1 +0,0 @@
Intl.PluralRules&&"function"==typeof Intl.PluralRules.__addLocaleData&&Intl.PluralRules.__addLocaleData({data:{categories:{cardinal:["one","other"],ordinal:["one","two","few","other"]},fn:function(e,l){var a=String(e).split("."),t=!a[1],o=Number(a[0])==e,n=o&&a[0].slice(-1),r=o&&a[0].slice(-2);return l?1==n&&11!=r?"one":2==n&&12!=r?"two":3==n&&13!=r?"few":"other":1==e&&t?"one":"other"}},locale:"en"});
@@ -1,7 +0,0 @@
import{a as r,h as e,e as t,t as s,$ as i,ah as o,n as a}from"./main-ad130be7.js";import{e as n,c}from"./c.50bfd408.js";const d={hacs_repository:{redirect:"/hacs/repository",params:{owner:"string",repository:"string",category:"string?"}}};r([a("hacs-my-redirect")],(function(r,e){return{F:class extends e{constructor(...e){super(...e),r(this)}},d:[{kind:"field",decorators:[t({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[t({attribute:!1})],key:"hacs",value:void 0},{kind:"field",decorators:[t({attribute:!1})],key:"route",value:void 0},{kind:"field",decorators:[s()],key:"_error",value:void 0},{kind:"method",key:"firstUpdated",value:function(r){const e=this.route.path.indexOf("/",1),t=this.route.path.substr(e+1),s=d[t];if(!s)return void(this._error=this.hacs.localize("my.not_supported",{link:i`<a
target="_blank"
rel="noreferrer noopener"
href="https://my.home-assistant.io/faq.html#supported-pages"
>
${this.hacs.localize("my.faq_link")}
</a>`}));let a;try{a=this._createRedirectUrl(s)}catch(r){return void(this._error=this.hacs.localize("my.error"))}o(a,{replace:!0})}},{kind:"method",key:"render",value:function(){return this._error?i`<hass-error-screen .error=${this._error}></hass-error-screen>`:i``}},{kind:"method",key:"_createRedirectUrl",value:function(r){const e=this._createRedirectParams(r);return`${r.redirect}${e}`}},{kind:"method",key:"_createRedirectParams",value:function(r){const e=n();if(!r.params&&!Object.keys(e).length)return"";const t={};for(const[s,i]of Object.entries(r.params||{}))if(e[s]||!i.endsWith("?")){if(!e[s]||!this._checkParamType(i,e[s]))throw Error();t[s]=e[s]}return`?${c(t)}`}},{kind:"method",key:"_checkParamType",value:function(r,e){return"string"===r||"string?"===r}}]}}),e);export{d as REDIRECTS};
@@ -1,436 +0,0 @@
import{aw as e,ax as t,ay as i,a6 as a,a7 as o,a8 as s,a as n,h as r,e as l,$ as d,o as c,r as h,n as p,m as g,az as u,ah as m,aA as v,c as f,aB as y,aC as b,aD as w,d as k}from"./main-ad130be7.js";import"./c.82eccc94.js";import{A as x}from"./c.bc5a73e9.js";import{i as $}from"./c.21c042d4.js";import{c as _}from"./c.4a97632a.js";import"./c.f1291e50.js";import"./c.2d5ed670.js";import"./c.11ad1623.js";import{b as z}from"./c.0a1cf8d0.js";import{s as C}from"./c.2645c235.js";import"./c.8e28b461.js";import"./c.f6611997.js";import"./c.743a15a1.js";import"./c.5d3ce9d6.js";import"./c.4266acdb.js";customElements.define("ha-icon-next",class extends e{connectedCallback(){super.connectedCallback(),setTimeout((()=>{this.path="ltr"===window.getComputedStyle(this).direction?t:i}),100)}}),a({_template:o`
<style>
:host {
display: block;
/**
* Force app-header-layout to have its own stacking context so that its parent can
* control the stacking of it relative to other elements (e.g. app-drawer-layout).
* This could be done using \`isolation: isolate\`, but that's not well supported
* across browsers.
*/
position: relative;
z-index: 0;
}
#wrapper ::slotted([slot=header]) {
@apply --layout-fixed-top;
z-index: 1;
}
#wrapper.initializing ::slotted([slot=header]) {
position: relative;
}
:host([has-scrolling-region]) {
height: 100%;
}
:host([has-scrolling-region]) #wrapper ::slotted([slot=header]) {
position: absolute;
}
:host([has-scrolling-region]) #wrapper.initializing ::slotted([slot=header]) {
position: relative;
}
:host([has-scrolling-region]) #wrapper #contentContainer {
@apply --layout-fit;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
:host([has-scrolling-region]) #wrapper.initializing #contentContainer {
position: relative;
}
:host([fullbleed]) {
@apply --layout-vertical;
@apply --layout-fit;
}
:host([fullbleed]) #wrapper,
:host([fullbleed]) #wrapper #contentContainer {
@apply --layout-vertical;
@apply --layout-flex;
}
#contentContainer {
/* Create a stacking context here so that all children appear below the header. */
position: relative;
z-index: 0;
}
@media print {
:host([has-scrolling-region]) #wrapper #contentContainer {
overflow-y: visible;
}
}
</style>
<div id="wrapper" class="initializing">
<slot id="headerSlot" name="header"></slot>
<div id="contentContainer">
<slot></slot>
</div>
</div>
`,is:"app-header-layout",behaviors:[x],properties:{hasScrollingRegion:{type:Boolean,value:!1,reflectToAttribute:!0}},observers:["resetLayout(isAttached, hasScrollingRegion)"],get header(){return s(this.$.headerSlot).getDistributedNodes()[0]},_updateLayoutStates:function(){var e=this.header;if(this.isAttached&&e){this.$.wrapper.classList.remove("initializing"),e.scrollTarget=this.hasScrollingRegion?this.$.contentContainer:this.ownerDocument.documentElement;var t=e.offsetHeight;this.hasScrollingRegion?(e.style.left="",e.style.right=""):requestAnimationFrame(function(){var t=this.getBoundingClientRect(),i=document.documentElement.clientWidth-t.right;e.style.left=t.left+"px",e.style.right=i+"px"}.bind(this));var i=this.$.contentContainer.style;e.fixed&&!e.condenses&&this.hasScrollingRegion?(i.marginTop=t+"px",i.paddingTop=""):(i.paddingTop=t+"px",i.marginTop="")}}});class E extends(customElements.get("app-header-layout")){static get template(){return o`
<style>
:host {
display: block;
/**
* Force app-header-layout to have its own stacking context so that its parent can
* control the stacking of it relative to other elements (e.g. app-drawer-layout).
* This could be done using \`isolation: isolate\`, but that's not well supported
* across browsers.
*/
position: relative;
z-index: 0;
}
#wrapper ::slotted([slot="header"]) {
@apply --layout-fixed-top;
z-index: 1;
}
#wrapper.initializing ::slotted([slot="header"]) {
position: relative;
}
:host([has-scrolling-region]) {
height: 100%;
}
:host([has-scrolling-region]) #wrapper ::slotted([slot="header"]) {
position: absolute;
}
:host([has-scrolling-region])
#wrapper.initializing
::slotted([slot="header"]) {
position: relative;
}
:host([has-scrolling-region]) #wrapper #contentContainer {
@apply --layout-fit;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
:host([has-scrolling-region]) #wrapper.initializing #contentContainer {
position: relative;
}
#contentContainer {
/* Create a stacking context here so that all children appear below the header. */
position: relative;
z-index: 0;
/* Using 'transform' will cause 'position: fixed' elements to behave like
'position: absolute' relative to this element. */
transform: translate(0);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
}
@media print {
:host([has-scrolling-region]) #wrapper #contentContainer {
overflow-y: visible;
}
}
</style>
<div id="wrapper" class="initializing">
<slot id="headerSlot" name="header"></slot>
<div id="contentContainer"><slot></slot></div>
<slot id="fab" name="fab"></slot>
</div>
`}}customElements.define("ha-app-layout",E),n([p("ha-config-section")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[l()],key:"isWide",value:()=>!1},{kind:"field",decorators:[l({type:Boolean})],key:"vertical",value:()=>!1},{kind:"field",decorators:[l({type:Boolean,attribute:"full-width"})],key:"fullWidth",value:()=>!1},{kind:"method",key:"render",value:function(){return d`
<div
class="content ${c({narrow:!this.isWide,"full-width":this.fullWidth})}"
>
<div class="header"><slot name="header"></slot></div>
<div
class="together layout ${c({narrow:!this.isWide,vertical:this.vertical||!this.isWide,horizontal:!this.vertical&&this.isWide})}"
>
<div class="intro"><slot name="introduction"></slot></div>
<div class="panel flex-auto"><slot></slot></div>
</div>
</div>
`}},{kind:"get",static:!0,key:"styles",value:function(){return h`
:host {
display: block;
}
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
.layout {
display: flex;
}
.horizontal {
flex-direction: row;
}
.vertical {
flex-direction: column;
}
.flex-auto {
flex: 1 1 auto;
}
.header {
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
}
.together {
margin-top: 32px;
}
.intro {
font-family: var(--paper-font-subhead_-_font-family);
-webkit-font-smoothing: var(
--paper-font-subhead_-_-webkit-font-smoothing
);
font-weight: var(--paper-font-subhead_-_font-weight);
line-height: var(--paper-font-subhead_-_line-height);
width: 100%;
opacity: var(--dark-primary-opacity);
font-size: 14px;
padding-bottom: 20px;
}
.horizontal .intro {
max-width: 400px;
margin-right: 40px;
}
.panel {
margin-top: -24px;
}
.panel ::slotted(*) {
margin-top: 24px;
display: block;
}
.narrow.content {
max-width: 640px;
}
.narrow .together {
margin-top: 20px;
}
.narrow .intro {
padding-bottom: 20px;
margin-right: 0;
max-width: 500px;
}
.full-width {
padding: 0;
}
.full-width .layout {
flex-direction: column;
}
`}}]}}),r);const j=g(((e,t)=>{var i,a;const o=[],s=[],n=[];var r,l;return e.repositories.forEach((t=>{var i;if("pending-restart"===t.status&&n.push(t),e.addedToLovelace(e,t)||s.push(t),t.installed&&null!==(i=e.removed.map((e=>e.repository)))&&void 0!==i&&i.includes(t.full_name)){const i=e.removed.find((e=>e.repository===t.full_name));o.push({name:e.localize("entry.messages.removed_repository",{repository:i.repository}),info:i.reason,severity:"warning",dialog:"remove",repository:t})}})),null!==(i=e.info)&&void 0!==i&&i.startup&&["setup","waiting","startup"].includes(e.info.stage)&&o.push({name:e.localize(`entry.messages.${e.info.stage}.title`),info:e.localize(`entry.messages.${e.info.stage}.content`),severity:"warning"}),null!==(a=e.info)&&void 0!==a&&a.disabled_reason?[{name:e.localize("entry.messages.disabled.title"),secondary:e.localize(`entry.messages.disabled.${null===(r=e.info)||void 0===r?void 0:r.disabled_reason}.title`),info:e.localize(`entry.messages.disabled.${null===(l=e.info)||void 0===l?void 0:l.disabled_reason}.description`),severity:"error"}]:(s.length>0&&o.push({name:e.localize("entry.messages.resources.title"),info:e.localize("entry.messages.resources.content",{number:s.length}),severity:"error"}),n.length>0&&o.push({name:e.localize("entry.messages.restart.title"),path:t?"/_my_redirect/server_controls":void 0,info:e.localize("entry.messages.restart.content",{number:n.length,pluralWording:1===n.length?e.localize("common.integration"):e.localize("common.integration_plural")}),severity:"error"}),o)}));let S=n([p("hacs-entry-panel")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[l({attribute:!1})],key:"hacs",value:void 0},{kind:"field",decorators:[l({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[l({attribute:!1})],key:"route",value:void 0},{kind:"field",decorators:[l({type:Boolean,reflect:!0})],key:"narrow",value:void 0},{kind:"field",decorators:[l({type:Boolean})],key:"isWide",value:void 0},{kind:"method",key:"render",value:function(){var e,t;const i=[],a=[],o=j(this.hacs,$(this.hass,"my"));return this.hacs.repositories.forEach((e=>{e.pending_upgrade&&i.push(e)})),o.forEach((e=>{a.push({iconPath:u,name:e.name,info:e.info,secondary:e.secondary,path:e.path||"",severity:e.severity,dialog:e.dialog,repository:e.repository})})),this.dispatchEvent(new CustomEvent("update-hacs",{detail:{messages:a,updates:i},bubbles:!0,composed:!0})),d`
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button .hass=${this.hass} .narrow=${this.narrow}></ha-menu-button>
<div main-title>${this.narrow?"HACS":"Home Assistant Community Store"}</div>
</app-toolbar>
</app-header>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide} full-width>
${0!==(null===(e=this.hacs.messages)||void 0===e?void 0:e.length)?this.hacs.messages.map((e=>d`
<ha-alert
.alertType=${e.severity}
.title=${e.secondary?`${e.name} - ${e.secondary}`:e.name}
.rtl=${_(this.hass)}
>
${e.info}
<mwc-button
slot="action"
.label=${e.path?this.hacs.localize("common.navigate"):e.dialog?this.hacs.localize("common.show"):""}
@click=${()=>e.path?m(e.path):this._openDialog(e)}
>
</mwc-button>
</ha-alert>
`)):(this.narrow,"")}
${0!==(null===(t=this.hacs.updates)||void 0===t?void 0:t.length)?d` <ha-card outlined>
<div class="title">${this.hacs.localize("common.updates")}</div>
<mwc-list>
${v(this.hacs.updates).map((e=>d`
<ha-clickable-list-item
graphic="avatar"
disableHref
twoline
@click=${()=>this._openUpdateDialog(e)}
>
${"integration"===e.category?d`
<img
loading="lazy"
.src=${z({domain:e.domain,darkOptimized:this.hass.themes.darkMode,type:"icon"})}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
slot="graphic"
/>
`:d`
<ha-svg-icon
slot="graphic"
path="${f}"
style="padding-left: 0; height: 40px; width: 40px;"
>
</ha-svg-icon>
`}
<span>${e.name}</span>
<span slot="secondary"
>${this.hacs.localize("sections.pending_repository_upgrade",{downloaded:e.installed_version,available:e.available_version})}</span
>
</ha-clickable-list-item>
`))}
</mwc-list>
</ha-card>`:""}
<ha-card outlined>
<mwc-list>
${this.hacs.sections.map((e=>d`
<ha-clickable-list-item
graphic="avatar"
twoline
.hasMeta=${!this.narrow}
href=${e.path}
>
<div
slot="graphic"
class=${e.iconColor?"icon-background":""}
.style="background-color: ${e.iconColor||"undefined"}"
>
<ha-svg-icon .path=${e.iconPath}></ha-svg-icon>
</div>
<span>${e.name}</span>
<span slot="secondary">${e.description}</span>
${this.narrow?"":d`<ha-icon-next slot="meta"></ha-icon-next>`}
</ha-clickable-list-item>
`))}
${$(this.hass,"my")&&$(this.hass,"hassio")?d`
<ha-clickable-list-item
graphic="avatar"
disableHref
twoline
@click=${this._openSupervisorDialog}
.hasMeta=${!this.narrow}
>
<div
class="icon-background"
slot="graphic"
style="background-color: rgb(64, 132, 205)"
>
<ha-svg-icon .path=${y}></ha-svg-icon>
</div>
<span>${this.hacs.localize("sections.addon.title")}</span>
<span slot="secondary"
>${this.hacs.localize("sections.addon.description")}</span
>
</ha-clickable-list-item>
`:""}
<ha-clickable-list-item
graphic="avatar"
twoline
@click=${this._openAboutDialog}
disableHref
>
<div
class="icon-background"
slot="graphic"
style="background-color: rgb(74, 89, 99)"
>
<ha-svg-icon .path=${b}></ha-svg-icon>
</div>
<span>${this.hacs.localize("sections.about.title")}</span>
<span slot="secondary">${this.hacs.localize("sections.about.description")}</span>
</ha-clickable-list-item>
</mwc-list>
</ha-card>
</ha-config-section>
</ha-app-layout>
`}},{kind:"method",key:"_onImageLoad",value:function(e){e.target.style.visibility="initial"}},{kind:"method",key:"_onImageError",value:function(e){e.target&&(e.target.outerHTML=`\n <div slot="item-icon" class="icon-background">\n <ha-svg-icon path="${f}" style="padding-left: 0; height: 40px; width: 40px;"></ha-svg-icon>\n </div>`)}},{kind:"method",key:"_openDialog",value:function(e){e.dialog&&("remove"==e.dialog&&(e.dialog="removed"),this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:e.dialog,repository:e.repository},bubbles:!0,composed:!0})))}},{kind:"method",key:"_openUpdateDialog",value:function(e){this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"update",repository:e.id},bubbles:!0,composed:!0}))}},{kind:"method",key:"_openAboutDialog",value:async function(){C(this,this.hacs)}},{kind:"method",key:"_openSupervisorDialog",value:async function(){this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"navigate",path:"/_my_redirect/supervisor"},bubbles:!0,composed:!0}))}},{kind:"get",static:!0,key:"styles",value:function(){return[w,k,h`
:host {
--mdc-list-vertical-padding: 0;
}
ha-card:last-child {
margin-bottom: env(safe-area-inset-bottom);
}
:host(:not([narrow])) ha-card:last-child {
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
ha-config-section {
margin: auto;
margin-top: -32px;
max-width: 600px;
}
ha-card {
overflow: hidden;
}
ha-card a {
text-decoration: none;
color: var(--primary-text-color);
}
a.button {
display: block;
color: var(--primary-color);
padding: 16px;
}
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
@media all and (max-width: 600px) {
ha-card {
border-width: 1px 0;
border-radius: 0;
box-shadow: unset;
}
ha-config-section {
margin-top: -42px;
}
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
display: block;
}
ha-svg-icon {
padding: 8px;
}
.icon-background {
border-radius: 50%;
}
.icon-background ha-svg-icon {
color: #fff;
}
ha-clickable-list-item {
cursor: pointer;
font-size: 16px;
padding: 0;
}
`]}}]}}),r);export{S as HacsEntryPanel};
@@ -1,74 +0,0 @@
import{a as i,h as a,e as t,t as o,i as s,$ as e,D as r,j as n,A as l,r as d,n as c}from"./main-ad130be7.js";import"./c.710a50fc.js";import"./c.8d4c35ad.js";import"./c.8e28b461.js";i([c("dialog-box")],(function(i,a){return{F:class extends a{constructor(...a){super(...a),i(this)}},d:[{kind:"field",decorators:[t({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[o()],key:"_params",value:void 0},{kind:"field",decorators:[s("ha-textfield")],key:"_textField",value:void 0},{kind:"method",key:"showDialog",value:async function(i){this._params=i}},{kind:"method",key:"closeDialog",value:function(){var i,a;return!(null!==(i=this._params)&&void 0!==i&&i.confirmation||null!==(a=this._params)&&void 0!==a&&a.prompt)&&(!this._params||(this._dismiss(),!0))}},{kind:"method",key:"render",value:function(){if(!this._params)return e``;const i=this._params.confirmation||this._params.prompt;return e`
<ha-dialog
open
?scrimClickAction=${i}
?escapeKeyAction=${i}
@closed=${this._dialogClosed}
defaultAction="ignore"
.heading=${e`${this._params.warning?e`<ha-svg-icon
.path=${r}
style="color: var(--warning-color)"
></ha-svg-icon> `:""}${this._params.title?this._params.title:this._params.confirmation&&this.hass.localize("ui.dialogs.generic.default_confirmation_title")}`}
>
<div>
${this._params.text?e`
<p class=${this._params.prompt?"no-bottom-padding":""}>
${this._params.text}
</p>
`:""}
${this._params.prompt?e`
<ha-textfield
dialogInitialFocus
value=${n(this._params.defaultValue)}
.placeholder=${n(this._params.placeholder)}
.label=${this._params.inputLabel?this._params.inputLabel:""}
.type=${this._params.inputType?this._params.inputType:"text"}
></ha-textfield>
`:""}
</div>
${i&&e`
<mwc-button @click=${this._dismiss} slot="secondaryAction">
${this._params.dismissText?this._params.dismissText:this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
`}
<mwc-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt}
slot="primaryAction"
>
${this._params.confirmText?this._params.confirmText:this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
</ha-dialog>
`}},{kind:"method",key:"_dismiss",value:function(){var i;null!==(i=this._params)&&void 0!==i&&i.cancel&&this._params.cancel(),this._close()}},{kind:"method",key:"_confirm",value:function(){var i;this._params.confirm&&this._params.confirm(null===(i=this._textField)||void 0===i?void 0:i.value);this._close()}},{kind:"method",key:"_dialogClosed",value:function(i){"ignore"!==i.detail.action&&this._dismiss()}},{kind:"method",key:"_close",value:function(){this._params&&(this._params=void 0,l(this,"dialog-closed",{dialog:this.localName}))}},{kind:"get",static:!0,key:"styles",value:function(){return d`
:host([inert]) {
pointer-events: initial !important;
cursor: initial !important;
}
a {
color: var(--primary-color);
}
p {
margin: 0;
color: var(--primary-text-color);
}
.no-bottom-padding {
padding-bottom: 0;
}
.secondary {
color: var(--secondary-text-color);
}
ha-dialog {
--mdc-dialog-heading-ink-color: var(--primary-text-color);
--mdc-dialog-content-ink-color: var(--primary-text-color);
/* Place above other dialogs */
--dialog-z-index: 104;
}
@media all and (min-width: 600px) {
ha-dialog {
--mdc-dialog-min-width: 400px;
}
}
ha-textfield {
width: 100%;
}
`}}]}}),a);
@@ -1,114 +0,0 @@
import{a as e,h as t,e as i,$ as o,o as r,z as s,A as n,r as a,n as c,C as l,D as d,E as u,F as p}from"./main-ad130be7.js";const y={info:l,warning:d,error:u,success:p};e([c("ha-alert")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[i()],key:"title",value:()=>""},{kind:"field",decorators:[i({attribute:"alert-type"})],key:"alertType",value:()=>"info"},{kind:"field",decorators:[i({type:Boolean})],key:"dismissable",value:()=>!1},{kind:"field",decorators:[i({type:Boolean})],key:"rtl",value:()=>!1},{kind:"method",key:"render",value:function(){return o`
<div
class="issue-type ${r({rtl:this.rtl,[this.alertType]:!0})}"
role="alert"
>
<div class="icon ${this.title?"":"no-title"}">
<slot name="icon">
<ha-svg-icon .path=${y[this.alertType]}></ha-svg-icon>
</slot>
</div>
<div class="content">
<div class="main-content">
${this.title?o`<div class="title">${this.title}</div>`:""}
<slot></slot>
</div>
<div class="action">
<slot name="action">
${this.dismissable?o`<ha-icon-button
@click=${this._dismiss_clicked}
label="Dismiss alert"
.path=${s}
></ha-icon-button>`:""}
</slot>
</div>
</div>
</div>
`}},{kind:"method",key:"_dismiss_clicked",value:function(){n(this,"alert-dismissed-clicked")}},{kind:"field",static:!0,key:"styles",value:()=>a`
.issue-type {
position: relative;
padding: 8px;
display: flex;
}
.issue-type.rtl {
flex-direction: row-reverse;
}
.issue-type::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0.12;
pointer-events: none;
content: "";
border-radius: 4px;
}
.icon {
z-index: 1;
}
.icon.no-title {
align-self: center;
}
.issue-type.rtl > .content {
flex-direction: row-reverse;
text-align: right;
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.action {
z-index: 1;
width: min-content;
--mdc-theme-primary: var(--primary-text-color);
}
.main-content {
overflow-wrap: anywhere;
word-break: break-word;
margin-left: 8px;
margin-right: 0;
}
.issue-type.rtl > .content > .main-content {
margin-left: 0;
margin-right: 8px;
}
.title {
margin-top: 2px;
font-weight: bold;
}
.action mwc-button,
.action ha-icon-button {
--mdc-theme-primary: var(--primary-text-color);
--mdc-icon-button-size: 36px;
}
.issue-type.info > .icon {
color: var(--info-color);
}
.issue-type.info::after {
background-color: var(--info-color);
}
.issue-type.warning > .icon {
color: var(--warning-color);
}
.issue-type.warning::after {
background-color: var(--warning-color);
}
.issue-type.error > .icon {
color: var(--error-color);
}
.issue-type.error::after {
background-color: var(--error-color);
}
.issue-type.success > .icon {
color: var(--success-color);
}
.issue-type.success::after {
background-color: var(--success-color);
}
`}]}}),t);
@@ -1 +0,0 @@
const t=(t,e)=>{const n={};return e&&(e.type&&(n.type_filter=e.type),e.domain&&(n.domain=e.domain)),t.callWS({type:"config_entries/get",...n})};export{t as g};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
const s=async(s,o)=>s.connection.sendMessagePromise({type:"hacs/repository/info",repository_id:o}),o=async(s,o,e)=>s.connection.sendMessagePromise({type:"hacs/repository/download",repository:o,version:e}),e=async(s,o,e)=>s.connection.sendMessagePromise({type:"hacs/repository/version",repository:o,version:e});export{o as a,s as f,e as r};
@@ -1,10 +1 @@
!function(){function n(n){var e=document.createElement("script");e.src=n,document.body.appendChild(e)}if(/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent))n("/hacsfiles/frontend/frontend_es5/entrypoint.4G_vEpsjfjQ.js");else try{new Function("import('/hacsfiles/frontend/frontend_latest/entrypoint.xkDQGhK7H8M.js')")()}catch(e){n("/hacsfiles/frontend/frontend_es5/entrypoint.4G_vEpsjfjQ.js")}}()
try {
new Function("import('/hacsfiles/frontend/main-ad130be7.js')")();
} catch (err) {
var el = document.createElement('script');
el.src = '/hacsfiles/frontend/main-ad130be7.js';
el.type = 'module';
document.body.appendChild(el);
}

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