Home Assistant Git Exporter
This commit is contained in:
@@ -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
@@ -1 +1 @@
|
||||
2024.8.0
|
||||
2024.8.3
|
||||
+187
-22
@@ -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
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
@@ -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."""
|
||||
|
||||
@@ -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",))
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user