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
slug: db21ed7f
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':
maintainer: sguernion
slug: 3bff5a27
+1 -1
View File
@@ -1 +1 @@
2024.8.0
2024.8.3
+187 -22
View File
@@ -22,12 +22,12 @@
{
"name": "Dates",
"id": "5b702fc3a47244c7bfce29e27eb6b19f",
"complete": false
"complete": true
},
{
"name": "Bananes",
"id": "6aa090cb21b141f8a2b37386e5896711",
"complete": false
"complete": true
},
{
"name": "Abricot",
@@ -104,26 +104,11 @@
"id": "78dfd45120b04d798ac63020635b096c",
"complete": true
},
{
"name": "Bleu",
"id": "6124a137218348009e641c02c7d36d9d",
"complete": true
},
{
"name": "Blanc",
"id": "2e603522f7dd4d50a31af1acd5986c89",
"complete": true
},
{
"name": "Vinaigre blanc",
"id": "a4f1a2acde9e4cf29804036b353d8a45",
"complete": true
},
{
"name": "Grain jaune mouche",
"id": "164ff5e89df14b13bc1e9fc4eccbf859",
"complete": true
},
{
"name": "eponge",
"id": "7bdaf185b84d408ca1bc510e3f184c8b",
@@ -174,11 +159,6 @@
"id": "6b3cff6926c14915b9bb1496c2d49a3e",
"complete": true
},
{
"name": "Aliment poule",
"id": "941e3e0206e04e34924ec1a0bde9a46b",
"complete": true
},
{
"name": "Cassis",
"id": "71f61483307245e9a05c4085daeff9ab",
@@ -217,6 +197,191 @@
{
"name": "Tomates grappes",
"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
}
]
+168 -56
View File
@@ -789,13 +789,17 @@
alias: arret auto ballon 2h
description: ''
trigger:
- platform: state
entity_id:
- switch.zb_30a_relay_chauffe_eau
from: 'off'
to: 'on'
- platform: device
type: turned_on
device_id: 2d23b7cb9abcf02cfb24d3c138c29f28
entity_id: 0f3c3b69a354e1c462e6176b973ac783
domain: switch
condition: []
action:
- type: turn_on
device_id: 2d23b7cb9abcf02cfb24d3c138c29f28
entity_id: 0f3c3b69a354e1c462e6176b973ac783
domain: switch
- delay:
hours: 2
minutes: 40
@@ -1408,18 +1412,18 @@
minutes: 6
seconds: 0
milliseconds: 0
- service: switch.turn_off
data: {}
target:
entity_id: switch.prise_salon_uv_z
- service: tts.speak
- data: {}
action: switch.turn_off
target:
entity_id: switch.prise_uv_zb
- target:
entity_id: tts.google_fr_fr
data:
cache: true
media_player_entity_id: media_player.hp_salon
message: cycle UV terminée
language: fr
action: tts.speak
mode: single
- id: '1691485011308'
alias: J arrive
@@ -1456,31 +1460,6 @@
message: l'impression sur l'elegoo est terminée
language: fr
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'
alias: restart esphome's
description: ''
@@ -1498,21 +1477,21 @@
- switch.eclairage_bois_restart
mode: single
- id: '1697147323532'
alias: 4switchz_1_cuisine
alias: bouton_rond_1_cuisine
description: ''
trigger:
- platform: device
domain: mqtt
device_id: 43d8325013319e1fd0fb56ab8b63ee7c
device_id: 919d9c0cf1d805314bcc290c7b5f4909
type: action
subtype: 1_single
subtype: single
condition: []
action:
- service: light.toggle
data:
- data:
brightness_pct: 67
target:
entity_id: light.applique_cuisine
action: light.toggle
mode: single
- id: '1697518688800'
description: ''
@@ -2046,22 +2025,6 @@
entity_id: a1c5260977db4e2f04bc3182d6effe2e
domain: switch
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'
alias: poussin eteint
description: ''
@@ -2294,3 +2257,152 @@
entity_id: 32ea2685688656a175fb0cc49a5cafb2
domain: switch
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.blitzortung_lightning_counter
- 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:
# - 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_CONFIG,
ATTR_COORDINATOR,
ATTR_WS_EVENT_PROXY,
ATTRIBUTE_LABELS,
CONF_CAMERA_STATIC_IMAGE_HEIGHT,
DOMAIN,
@@ -52,6 +53,7 @@ from .const import (
)
from .views import async_setup as views_async_setup
from .ws_api import async_setup as ws_api_async_setup
from .ws_event_proxy import WSEventProxy
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}"
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] = {
ATTR_COORDINATOR: coordinator,
ATTR_CLIENT: client,
ATTR_CONFIG: config,
ATTR_MODEL: model,
ATTR_WS_EVENT_PROXY: ws_event_proxy,
}
# 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_ARGUMENT = "argument"
ATTR_START_TIME = "start_time"
ATTR_WS_EVENT_PROXY = "ws_event_proxy"
# Frigate Attribute Labels
# These are labels that are not individually tracked as they are
@@ -14,5 +14,5 @@
"iot_class": "local_push",
"issue_tracker": "https://github.com/blakeblackshear/frigate-hass-integration/issues",
"requirements": ["pytz"],
"version": "5.3.0"
"version": "5.4.0"
}
+72 -2
View File
@@ -6,7 +6,12 @@ import logging
import voluptuous as vol
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.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_summary)
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(
@@ -157,7 +164,6 @@ async def ws_get_recordings_summary(
vol.Optional("limit"): int,
vol.Optional("has_clip"): bool,
vol.Optional("has_snapshot"): bool,
vol.Optional("has_snapshot"): bool,
vol.Optional("favorites"): bool,
}
) # 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(
{
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
https://hacs.xyz/
"""
from __future__ import annotations
import os
from typing import Any
from __future__ import annotations
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
from aiogithubapi.const import ACCEPT_HEADERS
from awesomeversion import AwesomeVersion
from homeassistant.components.frontend import async_remove_panel
from homeassistant.components.lovelace.system_health import system_health_info
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.start import async_at_start
from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase
from .const import DOMAIN, MINIMUM_HA_VERSION, STARTUP
from .const import DOMAIN, HACS_SYSTEM_ID, MINIMUM_HA_VERSION, STARTUP
from .data_client import HacsDataClient
from .enums import ConfigurationType, HacsDisabledReason, HacsStage, LovelaceMode
from .enums import HacsDisabledReason, HacsStage, LovelaceMode
from .frontend import async_register_frontend
from .utils.configuration_schema import hacs_config_combined
from .utils.data import HacsData
from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager
from .utils.version import version_left_higher_or_equal_then_right
from .websocket import async_register_websocket_commands
CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA)
PLATFORMS = [Platform.SWITCH, Platform.UPDATE]
async def async_initialize_integration(
async def _async_initialize_integration(
hass: HomeAssistant,
*,
config_entry: ConfigEntry | None = None,
config: dict[str, Any] | None = None,
config_entry: ConfigEntry,
) -> bool:
"""Initialize the integration"""
hass.data[DOMAIN] = hacs = HacsBase()
hacs.enable_hacs()
if config is not None:
if DOMAIN not in config:
return True
if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY:
return True
hacs.configuration.update_from_dict(
{
"config_type": ConfigurationType.YAML,
**config[DOMAIN],
"config": config[DOMAIN],
}
)
if config_entry.source == SOURCE_IMPORT:
# Import is not supported
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
if config_entry is not None:
if config_entry.source == SOURCE_IMPORT:
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
hacs.configuration.update_from_dict(
{
"config_entry": config_entry,
"config_type": ConfigurationType.CONFIG_ENTRY,
**config_entry.data,
**config_entry.options,
}
)
hacs.configuration.update_from_dict(
{
"config_entry": config_entry,
**config_entry.data,
**config_entry.options,
},
)
integration = await async_get_integration(hass, DOMAIN)
@@ -104,7 +82,6 @@ async def async_initialize_integration(
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
# If this happens, the users YAML is not valid, we assume YAML mode
pass
hacs.log.debug("Configuration type: %s", hacs.configuration.config_type)
hacs.core.config_path = hacs.hass.config.path()
if hacs.core.ha_version is None:
@@ -131,19 +108,18 @@ async def async_initialize_integration(
"""HACS startup tasks."""
hacs.enable_hacs()
for location in (
hass.config.path("custom_components/custom_updater.py"),
hass.config.path("custom_components/custom_updater/__init__.py"),
):
if os.path.exists(location):
hacs.log.critical(
"This cannot be used with custom_updater. "
"To use this you need to remove custom_updater form %s",
location,
)
try:
import custom_components.custom_updater
except ImportError:
pass
else:
hacs.log.critical(
"HACS cannot be used with custom_updater. "
"To use HACS you need to remove custom_updater from `custom_components`",
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not version_left_higher_or_equal_then_right(
hacs.core.ha_version.string,
@@ -160,39 +136,23 @@ async def async_initialize_integration(
hacs.disable_hacs(HacsDisabledReason.RESTORE)
return False
if not hacs.configuration.experimental:
can_update = await hacs.async_can_update()
hacs.log.debug("Can update %s repositories", can_update)
hacs.set_active_categories()
async_register_websocket_commands(hass)
async_register_frontend(hass, hacs)
await async_register_frontend(hass, hacs)
if hacs.configuration.config_type == ConfigurationType.YAML:
hass.async_create_task(
async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, hacs.configuration.config)
)
hacs.log.info("Update entities are only supported when using UI configuration")
else:
await hass.config_entries.async_forward_entry_setups(
config_entry,
[Platform.SENSOR, Platform.UPDATE]
if hacs.configuration.experimental
else [Platform.SENSOR],
)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hacs.set_stage(HacsStage.SETUP)
if hacs.system.disabled:
return False
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
hacs.set_stage(HacsStage.WAITING)
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
return not hacs.system.disabled
async def async_try_startup(_=None):
@@ -202,10 +162,7 @@ async def async_initialize_integration(
except AIOGitHubAPIException:
startup_result = False
if not startup_result:
if (
hacs.configuration.config_type == ConfigurationType.YAML
or hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN
):
if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN:
hacs.log.info("Could not setup HACS, trying again in 15 min")
async_call_later(hass, 900, async_try_startup)
return
@@ -213,37 +170,19 @@ async def async_initialize_integration(
await async_try_startup()
# Remove old (v0-v1) sensor if it exists, can be removed in v3
er = async_get_entity_registry(hass)
if old_sensor := er.async_get_entity_id("sensor", DOMAIN, HACS_SYSTEM_ID):
er.async_remove(old_sensor)
# Mischief managed!
return True
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up this integration using yaml."""
if DOMAIN in config:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_configuration",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_configuration",
learn_more_url="https://hacs.xyz/docs/configuration/options",
)
LOGGER.warning(
"YAML configuration of HACS is deprecated and will be "
"removed in version 2.0.0, there will be no automatic "
"import of this. "
"Please remove it from your configuration, "
"restart Home Assistant and use the UI to configure it instead."
)
return await async_initialize_integration(hass=hass, config=config)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
setup_result = await async_initialize_integration(hass=hass, config_entry=config_entry)
setup_result = await _async_initialize_integration(hass=hass, config_entry=config_entry)
hacs: HacsBase = hass.data[DOMAIN]
return setup_result and not hacs.system.disabled
@@ -259,7 +198,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
# Clear out pending queue
hacs.queue.clear()
for task in hacs.recuring_tasks:
for task in hacs.recurring_tasks:
# Cancel all pending tasks
task()
@@ -269,15 +208,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
try:
if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel")
hass.components.frontend.async_remove_panel("hacs")
async_remove_panel(hass, "hacs")
except AttributeError:
pass
platforms = ["sensor"]
if hacs.configuration.experimental:
platforms.append("update")
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, platforms)
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
hacs.set_stage(None)
hacs.disable_hacs(HacsDisabledReason.REMOVED)
+159 -233
View File
@@ -1,16 +1,17 @@
"""Base HACS class."""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field
from datetime import timedelta
import gzip
import logging
import math
import os
import pathlib
import shutil
from typing import TYPE_CHECKING, Any, Awaitable, Callable
from typing import TYPE_CHECKING, Any
from aiogithubapi import (
AIOGitHubAPIException,
@@ -24,23 +25,22 @@ from aiogithubapi import (
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
from aiohttp.client import ClientSession, ClientTimeout
from awesomeversion import AwesomeVersion
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.components.persistent_notification import (
async_create as async_create_persistent_notification,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.loader import Integration
from homeassistant.util import dt
from custom_components.hacs.repositories.base import (
HACS_MANIFEST_KEYS_TO_EXPORT,
REPOSITORY_KEYS_TO_EXPORT,
)
from .const import DOMAIN, TV, URL_BASE
from .coordinator import HacsUpdateCoordinator
from .data_client import HacsDataClient
from .enums import (
ConfigurationType,
HacsCategory,
HacsDisabledReason,
HacsDispatchEvent,
@@ -58,12 +58,14 @@ from .exceptions import (
HacsRepositoryExistException,
HomeAssistantCoreRepositoryException,
)
from .repositories import RERPOSITORY_CLASSES
from .utils.decode import decode_content
from .repositories import REPOSITORY_CLASSES
from .repositories.base import HACS_MANIFEST_KEYS_TO_EXPORT, REPOSITORY_KEYS_TO_EXPORT
from .utils.file_system import async_exists
from .utils.json import json_loads
from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager
from .utils.store import async_load_from_store, async_save_to_store
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING:
from .repositories.base import HacsRepository
@@ -113,15 +115,11 @@ class HacsConfiguration:
appdaemon: bool = False
config: dict[str, Any] = field(default_factory=dict)
config_entry: ConfigEntry | None = None
config_type: ConfigurationType | None = None
country: str = "ALL"
debug: bool = False
dev: bool = False
experimental: bool = False
frontend_repo_url: str = ""
frontend_repo: str = ""
netdaemon_path: str = "netdaemon/apps/"
netdaemon: bool = False
plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/"
python_script: bool = False
@@ -142,6 +140,8 @@ class HacsConfiguration:
raise HacsException("Configuration is not valid.")
for key in data:
if key in {"experimental", "netdaemon", "release_limit", "debug"}:
continue
self.__setattr__(key, data[key])
@@ -355,9 +355,6 @@ class HacsRepositories:
class HacsBase:
"""Base HACS class."""
common = HacsCommon()
configuration = HacsConfiguration()
core = HacsCore()
data: HacsData | None = None
data_client: HacsDataClient | None = None
frontend_version: str | None = None
@@ -365,18 +362,25 @@ class HacsBase:
githubapi: GitHubAPI | None = None
hass: HomeAssistant | None = None
integration: Integration | None = None
log: logging.Logger = LOGGER
queue: QueueManager | None = None
recuring_tasks = []
repositories: HacsRepositories = HacsRepositories()
repository: AIOGitHubAPIRepository | None = None
session: ClientSession | None = None
stage: HacsStage | None = None
status = HacsStatus()
system = HacsSystem()
validation: ValidationManager | None = None
version: AwesomeVersion | None = None
def __init__(self) -> None:
"""Initialize."""
self.common = HacsCommon()
self.configuration = HacsConfiguration()
self.coordinators: dict[HacsCategory, HacsUpdateCoordinator] = {}
self.core = HacsCore()
self.log = LOGGER
self.recurring_tasks: list[Callable[[], None]] = []
self.repositories = HacsRepositories()
self.status = HacsStatus()
self.system = HacsSystem()
@property
def integration_dir(self) -> pathlib.Path:
"""Return the HACS integration dir."""
@@ -401,12 +405,7 @@ class HacsBase:
if reason != HacsDisabledReason.REMOVED:
self.log.error("HACS is disabled - %s", reason)
if (
reason == HacsDisabledReason.INVALID_TOKEN
and self.configuration.config_type == ConfigurationType.CONFIG_ENTRY
):
self.configuration.config_entry.state = ConfigEntryState.SETUP_ERROR
self.configuration.config_entry.reason = "Authentication failed"
if reason == HacsDisabledReason.INVALID_TOKEN:
self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass)
def enable_hacs(self) -> None:
@@ -420,12 +419,14 @@ class HacsBase:
if category not in self.common.categories:
self.log.info("Enable category: %s", category)
self.common.categories.add(category)
self.coordinators[category] = HacsUpdateCoordinator()
def disable_hacs_category(self, category: HacsCategory) -> None:
"""Disable HACS category."""
if category in self.common.categories:
self.log.info("Disabling category: %s", category)
self.common.categories.pop(category)
self.coordinators.pop(category)
async def async_save_file(self, file_path: str, content: Any) -> bool:
"""Save a file."""
@@ -458,12 +459,13 @@ class HacsBase:
try:
await self.hass.async_add_executor_job(_write_file)
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as error:
self.log.error("Could not write data to %s - %s", file_path, error)
return False
return os.path.exists(file_path)
return await async_exists(self.hass, file_path)
async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for."""
@@ -479,24 +481,13 @@ class HacsBase:
)
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.log.exception(exception)
return 0
async def async_github_get_hacs_default_file(self, filename: str) -> list:
"""Get the content of a default file."""
response = await self.async_github_api_method(
method=self.githubapi.repos.contents.get,
repository=HacsGitHubRepo.DEFAULT,
path=filename,
)
if response is None:
return []
return json_loads(decode_content(response.data.content))
async def async_github_api_method(
self,
method: Callable[[], Awaitable[TV]],
@@ -520,7 +511,8 @@ class HacsBase:
except GitHubException as exception:
_exception = exception
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.log.exception(exception)
_exception = exception
@@ -552,7 +544,7 @@ class HacsBase:
):
raise AddonRepositoryException()
if category not in RERPOSITORY_CLASSES:
if category not in REPOSITORY_CLASSES:
self.log.warning(
"%s is not a valid repository category, %s will not be registered.",
category,
@@ -563,7 +555,7 @@ class HacsBase:
if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None:
repository_full_name = renamed
repository: HacsRepository = RERPOSITORY_CLASSES[category](self, repository_full_name)
repository: HacsRepository = REPOSITORY_CLASSES[category](self, repository_full_name)
if check:
try:
await repository.async_registration(ref)
@@ -573,7 +565,8 @@ class HacsBase:
self.log.error("Validation for %s failed.", repository_full_name)
if self.system.action:
raise HacsException(
f"::error:: Validation for {repository_full_name} failed."
f"::error:: Validation for {
repository_full_name} failed."
)
return repository.validate.errors
if self.system.action:
@@ -589,7 +582,8 @@ class HacsBase:
except AIOGitHubAPIException as exception:
self.common.skip.add(repository.data.full_name)
raise HacsException(
f"Validation for {repository_full_name} failed with {exception}."
f"Validation for {
repository_full_name} failed with {exception}."
) from exception
if self.status.new:
@@ -620,79 +614,64 @@ class HacsBase:
for repo in critical:
if not repo["acknowledged"]:
self.log.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create(
title="URGENT!", message="**Check the HACS panel!**"
async_create_persistent_notification(
self.hass, title="URGENT!", message="**Check the HACS panel!**"
)
break
if not self.configuration.experimental:
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_downloaded_repositories, timedelta(hours=48)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_all_repositories,
timedelta(hours=96),
)
)
else:
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_load_hacs_from_github,
timedelta(hours=48),
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_update_downloaded_custom_repositories, timedelta(hours=48)
self.recurring_tasks.append(
async_track_time_interval(
self.hass,
self.async_load_hacs_from_github,
timedelta(hours=48),
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_get_all_category_repositories, timedelta(hours=6)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_update_downloaded_custom_repositories, timedelta(hours=48)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_check_rate_limit, timedelta(minutes=5)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_prosess_queue, timedelta(minutes=10)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_get_all_category_repositories, timedelta(hours=6)
)
)
self.recuring_tasks.append(
self.hass.helpers.event.async_track_time_interval(
self.async_handle_critical_repositories, timedelta(hours=6)
self.recurring_tasks.append(
async_track_time_interval(self.hass, self.async_check_rate_limit, timedelta(minutes=5))
)
self.recurring_tasks.append(
async_track_time_interval(self.hass, self.async_process_queue, timedelta(minutes=10))
)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_handle_critical_repositories, timedelta(hours=6)
)
)
self.hass.bus.async_listen_once(
unsub = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write
)
if config_entry := self.configuration.config_entry:
config_entry.async_on_unload(unsub)
self.log.debug("There are %s scheduled recurring tasks", len(self.recuring_tasks))
self.log.debug("There are %s scheduled recurring tasks", len(self.recurring_tasks))
self.status.startup = False
self.async_dispatch(HacsDispatchEvent.STATUS, {})
await self.async_handle_removed_repositories()
await self.async_get_all_category_repositories()
await self.async_update_downloaded_repositories()
self.set_stage(HacsStage.RUNNING)
self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
await self.async_handle_critical_repositories()
await self.async_prosess_queue()
await self.async_process_queue()
self.async_dispatch(HacsDispatchEvent.STATUS, {})
@@ -728,9 +707,10 @@ class HacsBase:
return await request.read()
raise HacsException(
f"Got status code {request.status} when trying to download {url}"
f"Got status code {
request.status} when trying to download {url}"
)
except asyncio.TimeoutError:
except TimeoutError:
self.log.warning(
"A timeout of 60! seconds was encountered while downloading %s, "
"using over 60 seconds to download a single file is not normal. "
@@ -746,7 +726,8 @@ class HacsBase:
continue
except (
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
if not nolog:
self.log.exception("Download failed - %s", exception)
@@ -755,15 +736,24 @@ class HacsBase:
async def async_recreate_entities(self) -> None:
"""Recreate entities."""
if self.configuration == ConfigurationType.YAML or not self.configuration.experimental:
return
platforms = [Platform.UPDATE]
platforms = [Platform.SENSOR, Platform.UPDATE]
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
# Workaround for core versions without https://github.com/home-assistant/core/pull/117084
if self.core.ha_version < AwesomeVersion("2024.6.0"):
unload_platforms_lock = asyncio.Lock()
async with unload_platforms_lock:
on_unload = self.configuration.config_entry._on_unload
self.configuration.config_entry._on_unload = []
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
self.configuration.config_entry._on_unload = on_unload
else:
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
await self.hass.config_entries.async_forward_entry_setups(
self.configuration.config_entry, platforms
)
@@ -776,12 +766,9 @@ class HacsBase:
def set_active_categories(self) -> None:
"""Set the active categories."""
self.common.categories = set()
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN):
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN, HacsCategory.TEMPLATE):
self.enable_hacs_category(HacsCategory(category))
if self.configuration.experimental:
self.enable_hacs_category(HacsCategory.TEMPLATE)
if (
HacsCategory.PYTHON_SCRIPT in self.hass.config.components
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
@@ -795,30 +782,24 @@ class HacsBase:
if self.configuration.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:
"""Load HACS from GitHub."""
if self.configuration.experimental and self.status.inital_fetch_done:
if self.status.inital_fetch_done:
return
try:
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
should_recreate_entities = False
if repository is None:
should_recreate_entities = True
await self.async_register_repository(
repository_full_name=HacsGitHubRepo.INTEGRATION,
category=HacsCategory.INTEGRATION,
default=True,
)
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
elif self.configuration.experimental and not self.status.startup:
elif not self.status.startup:
self.log.error("Scheduling update of hacs/integration")
self.queue.add(repository.common_update())
if repository is None:
@@ -829,6 +810,9 @@ class HacsBase:
repository.data.new = False
repository.data.releases = True
if should_recreate_entities:
await self.async_recreate_entities()
self.repository = repository.repository_object
self.repositories.mark_default(repository)
except HacsException as exception:
@@ -848,8 +832,6 @@ class HacsBase:
await asyncio.gather(
*[
self.async_get_category_repositories_experimental(category)
if self.configuration.experimental
else self.async_get_category_repositories(HacsCategory(category))
for category in self.common.categories or []
]
)
@@ -858,7 +840,7 @@ class HacsBase:
"""Update all category repositories."""
self.log.debug("Fetching updated content for %s", category)
try:
category_data = await self.data_client.get_data(category)
category_data = await self.data_client.get_data(category, validate=True)
except HacsNotModifiedException:
self.log.debug("No updates for %s", category)
return
@@ -869,14 +851,14 @@ class HacsBase:
await self.data.register_unknown_repositories(category_data, category)
for repo_id, repo_data in category_data.items():
repo = repo_data["full_name"]
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if self.repositories.is_removed(repo):
repo_name = repo_data["full_name"]
if self.common.renamed_repositories.get(repo_name):
repo_name = self.common.renamed_repositories[repo_name]
if self.repositories.is_removed(repo_name):
continue
if repo in self.common.archived_repositories:
if repo_name in self.common.archived_repositories:
continue
if repository := self.repositories.get_by_full_name(repo):
if repository := self.repositories.get_by_full_name(repo_name):
self.repositories.set_repository_id(repository, repo_id)
self.repositories.mark_default(repository)
if repository.data.last_fetched is None or (
@@ -904,51 +886,7 @@ class HacsBase:
self.repositories.unregister(repository)
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
async def async_get_category_repositories(self, category: HacsCategory) -> None:
"""Get repositories from category."""
if self.system.disabled:
return
try:
repositories = await self.async_github_get_hacs_default_file(category)
except HacsException:
return
for repo in repositories:
if self.common.renamed_repositories.get(repo):
repo = self.common.renamed_repositories[repo]
if self.repositories.is_removed(repo):
continue
if repo in self.common.archived_repositories:
continue
repository = self.repositories.get_by_full_name(repo)
if repository is not None:
self.repositories.mark_default(repository)
if self.status.new and self.configuration.dev:
# Force update for new installations
self.queue.add(repository.common_update())
continue
self.queue.add(
self.async_register_repository(
repository_full_name=repo,
category=category,
default=True,
)
)
async def async_update_all_repositories(self, _=None) -> None:
"""Update all repositories."""
if self.system.disabled:
return
self.log.debug("Starting recurring background task for all repositories")
for repository in self.repositories.list_all:
if repository.data.category in self.common.categories:
self.queue.add(repository.common_update())
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {"action": "reload"})
self.log.debug("Recurring background task for all repositories done")
self.coordinators[category].async_update_listeners()
async def async_check_rate_limit(self, _=None) -> None:
"""Check rate limit."""
@@ -960,9 +898,9 @@ class HacsBase:
self.log.debug("Ratelimit indicate we can update %s", can_update)
if can_update > 0:
self.enable_hacs()
await self.async_prosess_queue()
await self.async_process_queue()
async def async_prosess_queue(self, _=None) -> None:
async def async_process_queue(self, _=None) -> None:
"""Process the queue."""
if self.system.disabled:
self.log.debug("HACS is disabled")
@@ -1002,12 +940,7 @@ class HacsBase:
self.log.info("Loading removed repositories")
try:
if self.configuration.experimental:
removed_repositories = await self.data_client.get_data("removed")
else:
removed_repositories = await self.async_github_get_hacs_default_file(
HacsCategory.REMOVED
)
removed_repositories = await self.data_client.get_data("removed", validate=True)
except HacsException:
return
@@ -1022,21 +955,20 @@ class HacsBase:
continue
if repository.data.installed:
if removed.removal_type != "critical":
if self.configuration.experimental:
async_create_issue(
hass=self.hass,
domain=DOMAIN,
issue_id=f"removed_{repository.data.id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="removed",
translation_placeholders={
"name": repository.data.full_name,
"reason": removed.reason,
"repositry_id": repository.data.id,
},
)
async_create_issue(
hass=self.hass,
domain=DOMAIN,
issue_id=f"removed_{repository.data.id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="removed",
translation_placeholders={
"name": repository.data.full_name,
"reason": removed.reason,
"repositry_id": repository.data.id,
},
)
self.log.warning(
"You have '%s' installed with HACS "
"this repository has been removed from HACS, please consider removing it. "
@@ -1051,30 +983,43 @@ class HacsBase:
if need_to_save:
await self.data.async_write()
async def async_update_downloaded_repositories(self, _=None) -> None:
"""Execute the task."""
if self.system.disabled or self.configuration.experimental:
return
self.log.info("Starting recurring background task for downloaded repositories")
for repository in self.repositories.list_downloaded:
if repository.data.category in self.common.categories:
self.queue.add(repository.update_repository(ignore_issues=True))
self.log.debug("Recurring background task for downloaded repositories done")
async def async_update_downloaded_custom_repositories(self, _=None) -> None:
"""Execute the task."""
if self.system.disabled or not self.configuration.experimental:
if self.system.disabled:
return
self.log.info("Starting recurring background task for downloaded custom repositories")
repositories_to_update = 0
repositories_updated = asyncio.Event()
async def update_repository(repository: HacsRepository) -> None:
"""Update a repository"""
nonlocal repositories_to_update
await repository.update_repository(ignore_issues=True)
repositories_to_update -= 1
if not repositories_to_update:
repositories_updated.set()
for repository in self.repositories.list_downloaded:
if (
repository.data.category in self.common.categories
and not self.repositories.is_default(repository.data.id)
):
self.queue.add(repository.update_repository(ignore_issues=True))
repositories_to_update += 1
self.queue.add(update_repository(repository))
async def update_coordinators() -> None:
"""Update all coordinators."""
await repositories_updated.wait()
for coordinator in self.coordinators.values():
coordinator.async_update_listeners()
if config_entry := self.configuration.config_entry:
config_entry.async_create_background_task(
self.hass, update_coordinators(), "update_coordinators"
)
else:
self.hass.async_create_background_task(update_coordinators(), "update_coordinators")
self.log.debug("Recurring background task for downloaded custom repositories done")
@@ -1086,10 +1031,7 @@ class HacsBase:
was_installed = False
try:
if self.configuration.experimental:
critical = await self.data_client.get_data("critical")
else:
critical = await self.async_github_get_hacs_default_file("critical")
critical = await self.data_client.get_data("critical", validate=True)
except (GitHubNotModifiedException, HacsNotModifiedException):
return
except HacsException:
@@ -1143,11 +1085,10 @@ class HacsBase:
self.log.critical("Restarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))
@callback
def async_setup_frontend_endpoint_plugin(self) -> None:
async def async_setup_frontend_endpoint_plugin(self) -> None:
"""Setup the http endpoints for plugins if its not already handled."""
if self.status.active_frontend_endpoint_plugin or not os.path.exists(
self.hass.config.path("www/community")
if self.status.active_frontend_endpoint_plugin or not await async_exists(
self.hass, self.hass.config.path("www/community")
):
return
@@ -1159,26 +1100,11 @@ class HacsBase:
use_cache,
)
self.hass.http.register_static_path(
await async_register_static_path(
self.hass,
URL_BASE,
self.hass.config.path("www/community"),
cache_headers=use_cache,
)
self.status.active_frontend_endpoint_plugin = True
@callback
def async_setup_frontend_endpoint_themes(self) -> None:
"""Setup the http endpoints for themes if its not already handled."""
if (
self.configuration.experimental
or self.status.active_frontend_endpoint_theme
or not os.path.exists(self.hass.config.path("themes"))
):
return
self.log.info("Setting up themes endpoint")
# Register themes
self.hass.http.register_static_path(f"{URL_BASE}/themes", self.hass.config.path("themes"))
self.status.active_frontend_endpoint_theme = True
+17 -40
View File
@@ -1,4 +1,5 @@
"""Adds config flow for HACS."""
from __future__ import annotations
import asyncio
@@ -23,14 +24,9 @@ import voluptuous as vol
from .base import HacsBase
from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION
from .enums import ConfigurationType
from .utils.configuration_schema import (
APPDAEMON,
COUNTRY,
DEBUG,
EXPERIMENTAL,
NETDAEMON,
RELEASE_LIMIT,
SIDEPANEL_ICON,
SIDEPANEL_TITLE,
)
@@ -75,15 +71,9 @@ class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_device(user_input)
## Initial form
# Initial form
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):
"""Handle device steps."""
@@ -97,8 +87,6 @@ class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
with suppress(UnknownFlow):
await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
self.hass.async_create_task(_progress())
if not self.device:
integration = await async_get_integration(self.hass, DOMAIN)
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="device_done")
return self.async_show_progress(
step_id="device",
progress_action="wait_for_device",
description_placeholders={
show_progress_kwargs = {
"step_id": "device",
"progress_action": "wait_for_device",
"description_placeholders": {
"url": OAUTH_USER_LOGIN,
"code": self._registration.user_code,
},
)
"progress_task": self.activation_task,
}
return self.async_show_progress(**show_progress_kwargs)
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
@@ -152,9 +142,6 @@ class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
"acc_untested", default=user_input.get("acc_untested", False)
): bool,
vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool,
vol.Optional(
"experimental", default=user_input.get("experimental", False)
): bool,
}
),
errors=self._errors,
@@ -176,7 +163,7 @@ class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
"token": self._activation.access_token,
},
options={
"experimental": self._user_input.get("experimental", False),
"experimental": True,
},
)
@@ -219,10 +206,7 @@ class HacsOptionsFlowHandler(OptionsFlow):
"""Handle a flow initialized by the user."""
hacs: HacsBase = self.hass.data.get(DOMAIN)
if user_input is not None:
limit = int(user_input.get(RELEASE_LIMIT, 5))
if limit <= 0 or limit > 100:
return self.async_abort(reason="release_limit_value")
return self.async_create_entry(title="", data=user_input)
return self.async_create_entry(title="", data={**user_input, "experimental": True})
if hacs is None or hacs.configuration is None:
return self.async_abort(reason="not_setup")
@@ -230,18 +214,11 @@ class HacsOptionsFlowHandler(OptionsFlow):
if hacs.queue.has_pending_tasks:
return self.async_abort(reason="pending_tasks")
if hacs.configuration.config_type == ConfigurationType.YAML:
schema = {vol.Optional("not_in_use", default=""): str}
else:
schema = {
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
vol.Optional(RELEASE_LIMIT, default=hacs.configuration.release_limit): int,
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
vol.Optional(NETDAEMON, default=hacs.configuration.netdaemon): bool,
vol.Optional(DEBUG, default=hacs.configuration.debug): bool,
vol.Optional(EXPERIMENTAL, default=hacs.configuration.experimental): bool,
}
schema = {
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
}
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))
+2 -1
View File
@@ -1,4 +1,5 @@
"""Constants for HACS"""
from typing import TypeVar
from aiogithubapi.common.const import ACCEPT_HEADERS
@@ -6,7 +7,7 @@ from aiogithubapi.common.const import ACCEPT_HEADERS
NAME_SHORT = "HACS"
DOMAIN = "hacs"
CLIENT_ID = "395a8e669c5de9f7c6e8"
MINIMUM_HA_VERSION = "2023.6.0"
MINIMUM_HA_VERSION = "2024.4.1"
URL_BASE = "/hacsfiles"
@@ -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."""
from __future__ import annotations
import asyncio
from typing import Any
from aiohttp import ClientSession, ClientTimeout
import voluptuous as vol
from .exceptions import HacsException, HacsNotModifiedException
from .utils.logger import LOGGER
from .utils.validate import (
VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REPO_DATA,
)
CRITICAL_REMOVED_VALIDATORS = {
"critical": VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
"removed": VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
}
class HacsDataClient:
@@ -39,7 +52,7 @@ class HacsDataClient:
response.raise_for_status()
except HacsNotModifiedException:
raise
except asyncio.TimeoutError:
except TimeoutError:
raise HacsException("Timeout of 60s reached") from None
except Exception as exception:
raise HacsException(f"Error fetching data from HACS: {exception}") from exception
@@ -48,9 +61,37 @@ class HacsDataClient:
return await response.json()
async def get_data(self, section: str | None) -> dict[str, dict[str, Any]]:
async def get_data(self, section: str | None, *, validate: bool) -> dict[str, dict[str, Any]]:
"""Get data."""
return await self._do_request(filename="data.json", section=section)
data = await self._do_request(filename="data.json", section=section)
if not validate:
return data
if section in VALIDATE_FETCHED_V2_REPO_DATA:
validated = {}
for key, repo_data in data.items():
try:
validated[key] = VALIDATE_FETCHED_V2_REPO_DATA[section](repo_data)
except vol.Invalid as exception:
LOGGER.info(
"Got invalid data for %s (%s)", repo_data.get("full_name", key), exception
)
continue
return validated
if not (validator := CRITICAL_REMOVED_VALIDATORS.get(section)):
raise ValueError(f"Do not know how to validate {section}")
validated = []
for repo_data in data:
try:
validated.append(validator(repo_data))
except vol.Invalid as exception:
LOGGER.info("Got invalid data for %s (%s)", section, exception)
continue
return validated
async def get_repositories(self, section: str) -> list[str]:
"""Get repositories."""
+2 -4
View File
@@ -1,4 +1,5 @@
"""Diagnostics support for HACS."""
from __future__ import annotations
from typing import Any
@@ -10,7 +11,6 @@ from homeassistant.core import HomeAssistant
from .base import HacsBase
from .const import DOMAIN
from .utils.configuration_schema import TOKEN
async def async_get_config_entry_diagnostics(
@@ -48,8 +48,6 @@ async def async_get_config_entry_diagnostics(
"country",
"debug",
"dev",
"experimental",
"netdaemon",
"python_script",
"release_limit",
"theme",
@@ -79,4 +77,4 @@ async def async_get_config_entry_diagnostics(
except GitHubException as exception:
data["rate_limit"] = str(exception)
return async_redact_data(data, (TOKEN,))
return async_redact_data(data, ("token",))
+36 -12
View File
@@ -1,4 +1,5 @@
"""HACS Base entities."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
@@ -7,8 +8,10 @@ from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity
from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
from .coordinator import HacsUpdateCoordinator
from .enums import HacsDispatchEvent, HacsGitHubRepo
if TYPE_CHECKING:
@@ -39,6 +42,10 @@ class HacsBaseEntity(Entity):
"""Initialize."""
self.hacs = hacs
class HacsDispatcherEntity(HacsBaseEntity):
"""Base HACS entity listening to dispatcher signals."""
async def async_added_to_hass(self) -> None:
"""Register for status events."""
self.async_on_remove(
@@ -64,7 +71,7 @@ class HacsBaseEntity(Entity):
self.async_write_ha_state()
class HacsSystemEntity(HacsBaseEntity):
class HacsSystemEntity(HacsDispatcherEntity):
"""Base system entity."""
_attr_icon = "hacs:hacs"
@@ -76,7 +83,7 @@ class HacsSystemEntity(HacsBaseEntity):
return system_info(self.hacs)
class HacsRepositoryEntity(HacsBaseEntity):
class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity):
"""Base repository entity."""
def __init__(
@@ -85,9 +92,11 @@ class HacsRepositoryEntity(HacsBaseEntity):
repository: HacsRepository,
) -> None:
"""Initialize."""
super().__init__(hacs=hacs)
BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category])
HacsBaseEntity.__init__(self, hacs=hacs)
self.repository = repository
self._attr_unique_id = str(repository.data.id)
self._repo_last_fetched = repository.data.last_fetched
@property
def available(self) -> bool:
@@ -100,20 +109,35 @@ class HacsRepositoryEntity(HacsBaseEntity):
if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION:
return system_info(self.hacs)
def _manufacturer():
if authors := self.repository.data.authors:
return ", ".join(author.replace("@", "") for author in authors)
return self.repository.data.full_name.split("/")[0]
return {
"identifiers": {(DOMAIN, str(self.repository.data.id))},
"name": self.repository.display_name,
"model": self.repository.data.category,
"manufacturer": ", ".join(
author.replace("@", "") for author in self.repository.data.authors
),
"configuration_url": "homeassistant://hacs",
"manufacturer": _manufacturer(),
"configuration_url": f"homeassistant://hacs/repository/{self.repository.data.id}",
"entry_type": DeviceEntryType.SERVICE,
}
@callback
def _update_and_write_state(self, data: dict) -> None:
"""Update the entity and write state."""
if data.get("repository_id") == self.repository.data.id:
self._update()
self.async_write_ha_state()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if (
self._repo_last_fetched is not None
and self.repository.data.last_fetched is not None
and self._repo_last_fetched >= self.repository.data.last_fetched
):
return
self._repo_last_fetched = self.repository.data.last_fetched
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
+2 -21
View File
@@ -1,20 +1,7 @@
"""Helper constants."""
# pylint: disable=missing-class-docstring
import sys
if sys.version_info.minor >= 11:
# Needs Python 3.11
from enum import StrEnum # # pylint: disable=no-name-in-module
else:
try:
# https://github.com/home-assistant/core/blob/dev/homeassistant/backports/enum.py
# Considered internal to Home Assistant, can be removed whenever.
from homeassistant.backports.enum import StrEnum
except ImportError:
from enum import Enum
class StrEnum(str, Enum):
pass
from enum import StrEnum
class HacsGitHubRepo(StrEnum):
@@ -29,7 +16,6 @@ class HacsCategory(StrEnum):
INTEGRATION = "integration"
LOVELACE = "lovelace"
PLUGIN = "plugin" # Kept for legacy purposes
NETDAEMON = "netdaemon"
PYTHON_SCRIPT = "python_script"
TEMPLATE = "template"
THEME = "theme"
@@ -59,11 +45,6 @@ class RepositoryFile(StrEnum):
MAINIFEST_JSON = "manifest.json"
class ConfigurationType(StrEnum):
YAML = "yaml"
CONFIG_ENTRY = "config_entry"
class LovelaceMode(StrEnum):
"""Lovelace Modes."""
+22 -40
View File
@@ -1,71 +1,53 @@
""""Starting setup task: Frontend"."""
"""Starting setup task: Frontend."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant, callback
from homeassistant.components.frontend import (
add_extra_js_url,
async_register_built_in_panel,
)
from .const import DOMAIN, URL_BASE
from .hacs_frontend import VERSION as FE_VERSION, locate_dir
from .hacs_frontend_experimental import (
VERSION as EXPERIMENTAL_FE_VERSION,
locate_dir as experimental_locate_dir,
)
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)
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from .base import HacsBase
@callback
def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
"""Register the frontend."""
# Setup themes endpoint if needed
hacs.async_setup_frontend_endpoint_themes()
# Register frontend
if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")):
hacs.log.warning(
"<HacsFrontend> Frontend development mode enabled. Do not run in production!"
)
hass.http.register_static_path(
f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
)
elif hacs.configuration.experimental:
hacs.log.info("<HacsFrontend> Using experimental frontend")
hass.http.register_static_path(
f"{URL_BASE}/frontend", experimental_locate_dir(), cache_headers=False
await async_register_static_path(
hass, f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
)
hacs.frontend_version = "dev"
else:
#
hass.http.register_static_path(f"{URL_BASE}/frontend", locate_dir(), cache_headers=False)
await async_register_static_path(
hass, f"{URL_BASE}/frontend", locate_dir(), cache_headers=False
)
hacs.frontend_version = FE_VERSION
# Custom iconset
hass.http.register_static_path(
f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
await async_register_static_path(
hass, f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
)
add_extra_js_url(hass, f"{URL_BASE}/iconset.js")
hacs.frontend_version = (
FE_VERSION if not hacs.configuration.experimental else EXPERIMENTAL_FE_VERSION
)
# Add to sidepanel if needed
if DOMAIN not in hass.data.get("frontend_panels", {}):
hass.components.frontend.async_register_built_in_panel(
async_register_built_in_panel(
hass,
component_name="custom",
sidebar_title=hacs.configuration.sidepanel_title,
sidebar_icon=hacs.configuration.sidepanel_icon,
@@ -82,4 +64,4 @@ def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
)
# Setup plugin endpoint if needed
hacs.async_setup_frontend_endpoint_plugin()
await hacs.async_setup_frontend_endpoint_plugin()
File diff suppressed because one or more lines are too long
@@ -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 @@
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);
}
!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")}}()

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