mirror of
https://github.com/home-assistant/core.git
synced 2026-03-25 17:10:32 +01:00
Compare commits
29 Commits
add-automa
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b522db1daf | ||
|
|
338836cba2 | ||
|
|
f5e7605502 | ||
|
|
22ddb18ce2 | ||
|
|
b541dc0a97 | ||
|
|
15d0a01833 | ||
|
|
71be2073eb | ||
|
|
e6886fc562 | ||
|
|
7f0f038bcd | ||
|
|
686ab66a52 | ||
|
|
7a4f953fa6 | ||
|
|
cd0834bfbe | ||
|
|
c598aa6964 | ||
|
|
5ef28932e5 | ||
|
|
f2eac87673 | ||
|
|
aeb920e8ef | ||
|
|
8540a27f0d | ||
|
|
fe2d8a31b8 | ||
|
|
f4efc929d6 | ||
|
|
15d7febffd | ||
|
|
0a8f5449f2 | ||
|
|
d2179d9243 | ||
|
|
bf1327e355 | ||
|
|
9afa827eab | ||
|
|
3ae6f8e7a0 | ||
|
|
56962ff907 | ||
|
|
719b9bdc3c | ||
|
|
bb1dc51a6b | ||
|
|
abbbb7df13 |
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||
@@ -40,6 +41,7 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
AbodeSensorDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
||||
device.temp_unit
|
||||
],
|
||||
@@ -48,12 +50,14 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
AbodeSensorDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
||||
value_fn=lambda device: cast(float, device.humidity),
|
||||
),
|
||||
AbodeSensorDescription(
|
||||
key="lux",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
||||
value_fn=lambda device: cast(float, device.lux),
|
||||
),
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
# --- Number or entity selectors ---
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity_co: &number_or_entity_co
|
||||
required: false
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_armed: *condition_common
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
armed: *trigger_common
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_idle: *condition_common
|
||||
is_listening: *condition_common
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
idle: *trigger_common
|
||||
listening: *trigger_common
|
||||
|
||||
@@ -155,6 +155,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"assist_satellite",
|
||||
"button",
|
||||
"climate",
|
||||
"counter",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==5.1.2"],
|
||||
"requirements": ["python-bsblan==5.1.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LIGHT]
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
||||
|
||||
73
homeassistant/components/casper_glow/button.py
Normal file
73
homeassistant/components/casper_glow/button.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Casper Glow integration button platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pycasperglow import CasperGlow
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CasperGlowButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describe a Casper Glow button entity."""
|
||||
|
||||
press_fn: Callable[[CasperGlow], Awaitable[None]]
|
||||
|
||||
|
||||
BUTTON_DESCRIPTIONS: tuple[CasperGlowButtonEntityDescription, ...] = (
|
||||
CasperGlowButtonEntityDescription(
|
||||
key="pause",
|
||||
translation_key="pause",
|
||||
press_fn=lambda device: device.pause(),
|
||||
),
|
||||
CasperGlowButtonEntityDescription(
|
||||
key="resume",
|
||||
translation_key="resume",
|
||||
press_fn=lambda device: device.resume(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the button platform for Casper Glow."""
|
||||
async_add_entities(
|
||||
CasperGlowButton(entry.runtime_data, description)
|
||||
for description in BUTTON_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class CasperGlowButton(CasperGlowEntity, ButtonEntity):
|
||||
"""A Casper Glow button entity."""
|
||||
|
||||
entity_description: CasperGlowButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CasperGlowCoordinator,
|
||||
description: CasperGlowButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Casper Glow button."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{format_mac(coordinator.device.address)}_{description.key}"
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._async_command(self.entity_description.press_fn(self._device))
|
||||
@@ -4,6 +4,14 @@
|
||||
"paused": {
|
||||
"default": "mdi:timer-pause"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"pause": {
|
||||
"default": "mdi:pause"
|
||||
},
|
||||
"resume": {
|
||||
"default": "mdi:play"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pycasperglow"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pycasperglow==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: Bluetooth device with no authentication credentials.
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
@@ -53,15 +55,9 @@ rules:
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: No entity translations needed.
|
||||
exception-translations:
|
||||
status: exempt
|
||||
comment: No custom services that raise exceptions.
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: No icon translations needed.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
@@ -31,6 +31,14 @@
|
||||
"paused": {
|
||||
"name": "Dimming paused"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"pause": {
|
||||
"name": "Pause dimming"
|
||||
},
|
||||
"resume": {
|
||||
"name": "Resume dimming"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -1,20 +1,68 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"chess960_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess960_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess960_daily_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess960_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_blitz_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_bullet_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_rapid_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"followers": {
|
||||
"default": "mdi:account-multiple"
|
||||
},
|
||||
"total_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"total_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"total_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from chess_com_api import PlayerStats
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -24,7 +27,14 @@ class ChessEntityDescription(SensorEntityDescription):
|
||||
value_fn: Callable[[ChessData], float]
|
||||
|
||||
|
||||
SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class ChessModeEntityDescription(SensorEntityDescription):
|
||||
"""Sensor description for a Chess.com game mode."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], float]
|
||||
|
||||
|
||||
PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
ChessEntityDescription(
|
||||
key="followers",
|
||||
translation_key="followers",
|
||||
@@ -33,35 +43,46 @@ SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
value_fn=lambda state: state.player.followers,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="chess_daily_rating",
|
||||
translation_key="chess_daily_rating",
|
||||
)
|
||||
|
||||
GAME_MODE_SENSORS: tuple[ChessModeEntityDescription, ...] = (
|
||||
ChessModeEntityDescription(
|
||||
key="rating",
|
||||
translation_key="rating",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda state: state.stats.chess_daily["last"]["rating"],
|
||||
value_fn=lambda mode: mode["last"]["rating"],
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="total_daily_won",
|
||||
translation_key="total_daily_won",
|
||||
ChessModeEntityDescription(
|
||||
key="won",
|
||||
translation_key="won",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["win"],
|
||||
value_fn=lambda mode: mode["record"]["win"],
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="total_daily_lost",
|
||||
translation_key="total_daily_lost",
|
||||
ChessModeEntityDescription(
|
||||
key="lost",
|
||||
translation_key="lost",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["loss"],
|
||||
value_fn=lambda mode: mode["record"]["loss"],
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="total_daily_draw",
|
||||
translation_key="total_daily_draw",
|
||||
ChessModeEntityDescription(
|
||||
key="draw",
|
||||
translation_key="draw",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["draw"],
|
||||
value_fn=lambda mode: mode["record"]["draw"],
|
||||
),
|
||||
)
|
||||
|
||||
GAME_MODES: dict[str, Callable[[PlayerStats], dict[str, Any] | None]] = {
|
||||
"chess_daily": lambda stats: stats.chess_daily,
|
||||
"chess_rapid": lambda stats: stats.chess_rapid,
|
||||
"chess_bullet": lambda stats: stats.chess_bullet,
|
||||
"chess_blitz": lambda stats: stats.chess_blitz,
|
||||
"chess960_daily": lambda stats: stats.chess960_daily,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -71,13 +92,22 @@ async def async_setup_entry(
|
||||
"""Initialize the entries."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
ChessPlayerSensor(coordinator, description) for description in SENSORS
|
||||
)
|
||||
entities: list[SensorEntity] = [
|
||||
ChessPlayerSensor(coordinator, description) for description in PLAYER_SENSORS
|
||||
]
|
||||
|
||||
for game_mode, stats_fn in GAME_MODES.items():
|
||||
if stats_fn(coordinator.data.stats) is not None:
|
||||
entities.extend(
|
||||
ChessGameModeSensor(coordinator, description, game_mode, stats_fn)
|
||||
for description in GAME_MODE_SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||
"""Chess.com sensor."""
|
||||
"""Chess.com player sensor."""
|
||||
|
||||
entity_description: ChessEntityDescription
|
||||
|
||||
@@ -95,3 +125,33 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
|
||||
class ChessGameModeSensor(ChessEntity, SensorEntity):
|
||||
"""Chess.com game mode sensor."""
|
||||
|
||||
entity_description: ChessModeEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ChessCoordinator,
|
||||
description: ChessModeEntityDescription,
|
||||
game_mode: str,
|
||||
stats_fn: Callable[[PlayerStats], dict[str, Any] | None],
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._stats_fn = stats_fn
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}.{game_mode}.{description.key}"
|
||||
)
|
||||
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
mode_data = self._stats_fn(self.coordinator.data.stats)
|
||||
if TYPE_CHECKING:
|
||||
assert mode_data is not None
|
||||
return self.entity_description.value_fn(mode_data)
|
||||
|
||||
@@ -23,24 +23,84 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"chess960_daily_draw": {
|
||||
"name": "Total daily Chess960 games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess960_daily_lost": {
|
||||
"name": "Total daily Chess960 games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess960_daily_rating": {
|
||||
"name": "Daily Chess960 rating"
|
||||
},
|
||||
"chess960_daily_won": {
|
||||
"name": "Total daily Chess960 games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_draw": {
|
||||
"name": "Total blitz chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_lost": {
|
||||
"name": "Total blitz chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_rating": {
|
||||
"name": "Blitz chess rating"
|
||||
},
|
||||
"chess_blitz_won": {
|
||||
"name": "Total blitz chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_draw": {
|
||||
"name": "Total bullet chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_lost": {
|
||||
"name": "Total bullet chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_rating": {
|
||||
"name": "Bullet chess rating"
|
||||
},
|
||||
"chess_bullet_won": {
|
||||
"name": "Total bullet chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_draw": {
|
||||
"name": "Total daily chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_lost": {
|
||||
"name": "Total daily chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_rating": {
|
||||
"name": "Daily chess rating"
|
||||
},
|
||||
"chess_daily_won": {
|
||||
"name": "Total daily chess games won",
|
||||
"unit_of_measurement": "games"
|
||||
},
|
||||
"chess_rapid_draw": {
|
||||
"name": "Total rapid chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_rapid_lost": {
|
||||
"name": "Total rapid chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_rapid_rating": {
|
||||
"name": "Rapid chess rating"
|
||||
},
|
||||
"chess_rapid_won": {
|
||||
"name": "Total rapid chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"followers": {
|
||||
"name": "Followers",
|
||||
"unit_of_measurement": "followers"
|
||||
},
|
||||
"total_daily_draw": {
|
||||
"name": "Total chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"total_daily_lost": {
|
||||
"name": "Total chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"total_daily_won": {
|
||||
"name": "Total chess games won",
|
||||
"unit_of_measurement": "games"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
.number_or_entity_humidity: &number_or_entity_humidity
|
||||
required: false
|
||||
|
||||
@@ -11,7 +11,8 @@ set_preset_mode:
|
||||
required: true
|
||||
example: "away"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: preset_mode
|
||||
|
||||
set_temperature:
|
||||
target:
|
||||
@@ -55,16 +56,10 @@ set_temperature:
|
||||
mode: box
|
||||
hvac_mode:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "off"
|
||||
- "auto"
|
||||
- "cool"
|
||||
- "dry"
|
||||
- "fan_only"
|
||||
- "heat_cool"
|
||||
- "heat"
|
||||
translation_key: hvac_mode
|
||||
state:
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
set_humidity:
|
||||
target:
|
||||
entity:
|
||||
@@ -91,7 +86,8 @@ set_fan_mode:
|
||||
required: true
|
||||
example: "low"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: fan_mode
|
||||
|
||||
set_hvac_mode:
|
||||
target:
|
||||
@@ -115,7 +111,8 @@ set_swing_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: swing_mode
|
||||
|
||||
set_swing_horizontal_mode:
|
||||
target:
|
||||
@@ -128,7 +125,8 @@ set_swing_horizontal_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: swing_horizontal_mode
|
||||
|
||||
turn_on:
|
||||
target:
|
||||
|
||||
@@ -281,17 +281,6 @@
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"hvac_mode": {
|
||||
"options": {
|
||||
"auto": "[%key:common::state::auto%]",
|
||||
"cool": "Cool",
|
||||
"dry": "Dry",
|
||||
"fan_only": "Fan only",
|
||||
"heat": "Heat",
|
||||
"heat_cool": "Heat/cool",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity_humidity: &number_or_entity_humidity
|
||||
required: false
|
||||
|
||||
@@ -12,5 +12,22 @@
|
||||
"set_value": {
|
||||
"service": "mdi:counter"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"decremented": {
|
||||
"trigger": "mdi:numeric-negative-1"
|
||||
},
|
||||
"incremented": {
|
||||
"trigger": "mdi:numeric-positive-1"
|
||||
},
|
||||
"maximum_reached": {
|
||||
"trigger": "mdi:sort-numeric-ascending-variant"
|
||||
},
|
||||
"minimum_reached": {
|
||||
"trigger": "mdi:sort-numeric-descending-variant"
|
||||
},
|
||||
"reset": {
|
||||
"trigger": "mdi:refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted counters to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::counter::title%]",
|
||||
@@ -25,6 +29,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"decrement": {
|
||||
"description": "Decrements a counter by its step size.",
|
||||
@@ -49,5 +62,45 @@
|
||||
"name": "Set"
|
||||
}
|
||||
},
|
||||
"title": "Counter"
|
||||
"title": "Counter",
|
||||
"triggers": {
|
||||
"decremented": {
|
||||
"description": "Triggers after one or more counters decrement.",
|
||||
"name": "Counter decremented"
|
||||
},
|
||||
"incremented": {
|
||||
"description": "Triggers after one or more counters increment.",
|
||||
"name": "Counter incremented"
|
||||
},
|
||||
"maximum_reached": {
|
||||
"description": "Triggers after one or more counters reach their maximum value.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::counter::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reached maximum"
|
||||
},
|
||||
"minimum_reached": {
|
||||
"description": "Triggers after one or more counters reach their minimum value.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::counter::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reached minimum"
|
||||
},
|
||||
"reset": {
|
||||
"description": "Triggers after one or more counters are reset.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::counter::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reset"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
homeassistant/components/counter/trigger.py
Normal file
113
homeassistant/components/counter/trigger.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Provides triggers for counters."""
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
)
|
||||
|
||||
from . import CONF_INITIAL, DOMAIN
|
||||
|
||||
|
||||
def _is_integer_state(state: State) -> bool:
|
||||
"""Return True if the state's value can be interpreted as an integer."""
|
||||
try:
|
||||
int(state.state)
|
||||
except TypeError, ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class CounterBaseIntegerTrigger(EntityTriggerBase):
|
||||
"""Base trigger for valid counter integer states."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state is valid."""
|
||||
return _is_integer_state(state)
|
||||
|
||||
|
||||
class CounterDecrementedTrigger(CounterBaseIntegerTrigger):
|
||||
"""Trigger for when a counter is decremented."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return int(from_state.state) > int(to_state.state)
|
||||
|
||||
|
||||
class CounterIncrementedTrigger(CounterBaseIntegerTrigger):
|
||||
"""Trigger for when a counter is incremented."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return int(from_state.state) < int(to_state.state)
|
||||
|
||||
|
||||
class CounterValueBaseTrigger(EntityTriggerBase):
|
||||
"""Base trigger for counter value changes."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return from_state.state != to_state.state
|
||||
|
||||
|
||||
class CounterMaxReachedTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for when a counter reaches its maximum value."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (max_value := state.attributes.get(CONF_MAXIMUM)) is None:
|
||||
return False
|
||||
return state.state == str(max_value)
|
||||
|
||||
|
||||
class CounterMinReachedTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for when a counter reaches its minimum value."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (min_value := state.attributes.get(CONF_MINIMUM)) is None:
|
||||
return False
|
||||
return state.state == str(min_value)
|
||||
|
||||
|
||||
class CounterResetTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for reset of counter entities."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (init_state := state.attributes.get(CONF_INITIAL)) is None:
|
||||
return False
|
||||
return state.state == str(init_state)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"decremented": CounterDecrementedTrigger,
|
||||
"incremented": CounterIncrementedTrigger,
|
||||
"maximum_reached": CounterMaxReachedTrigger,
|
||||
"minimum_reached": CounterMinReachedTrigger,
|
||||
"reset": CounterResetTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for counters."""
|
||||
return TRIGGERS
|
||||
27
homeassistant/components/counter/triggers.yaml
Normal file
27
homeassistant/components/counter/triggers.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
incremented:
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
decremented:
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
maximum_reached: *trigger_common
|
||||
minimum_reached: *trigger_common
|
||||
reset: *trigger_common
|
||||
@@ -3,9 +3,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
awning_is_closed:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
awning_closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_home: *condition_common
|
||||
is_not_home: *condition_common
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
entered_home: *trigger_common
|
||||
left_home: *trigger_common
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_closed:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -10,7 +10,8 @@ set_preset_mode:
|
||||
required: true
|
||||
example: "auto"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: preset_mode
|
||||
|
||||
set_percentage:
|
||||
target:
|
||||
@@ -49,7 +50,8 @@ turn_on:
|
||||
supported_features:
|
||||
- fan.FanEntityFeature.PRESET_MODE
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: preset_mode
|
||||
|
||||
turn_off:
|
||||
target:
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
turned_on: *trigger_common
|
||||
turned_off: *trigger_common
|
||||
|
||||
@@ -20,5 +20,7 @@ async def async_get_solar_forecast(
|
||||
"wh_hours": {
|
||||
timestamp.isoformat(): val
|
||||
for timestamp, val in entry.runtime_data.data.wh_period.items()
|
||||
if val != 0
|
||||
or (timestamp.hour, timestamp.minute, timestamp.second) != (0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_closed:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_closed:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -666,7 +666,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
|
||||
# Init add-on ingress panels
|
||||
panels_task = hass.async_create_task(
|
||||
async_setup_addon_panel(hass, hassio), eager_start=True
|
||||
async_setup_addon_panel(hass), eager_start=True
|
||||
)
|
||||
|
||||
# Make sure to await the update_info task before
|
||||
|
||||
@@ -2,24 +2,23 @@
|
||||
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import IngressPanel
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import ATTR_ICON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE
|
||||
from .handler import HassIO, HassioAPIError
|
||||
from .handler import get_supervisor_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_addon_panel(hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
async def async_setup_addon_panel(hass: HomeAssistant) -> None:
|
||||
"""Add-on Ingress Panel setup."""
|
||||
hassio_addon_panel = HassIOAddonPanel(hass, hassio)
|
||||
hassio_addon_panel = HassIOAddonPanel(hass)
|
||||
hass.http.register_view(hassio_addon_panel)
|
||||
|
||||
# If panels are exists
|
||||
@@ -28,11 +27,8 @@ async def async_setup_addon_panel(hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
|
||||
# Register available panels
|
||||
for addon, data in panels.items():
|
||||
if not data[ATTR_ENABLE]:
|
||||
if not data.enable:
|
||||
continue
|
||||
# _register_panel never suspends and is only
|
||||
# a coroutine because it would be a breaking change
|
||||
# to make it a normal function
|
||||
_register_panel(hass, addon, data)
|
||||
|
||||
|
||||
@@ -42,23 +38,22 @@ class HassIOAddonPanel(HomeAssistantView):
|
||||
name = "api:hassio_push:panel"
|
||||
url = "/api/hassio_push/panel/{addon}"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize WebView."""
|
||||
self.hass = hass
|
||||
self.hassio = hassio
|
||||
self.client = get_supervisor_client(hass)
|
||||
|
||||
async def post(self, request: web.Request, addon: str) -> web.Response:
|
||||
"""Handle new add-on panel requests."""
|
||||
panels = await self.get_panels()
|
||||
|
||||
# Panel exists for add-on slug
|
||||
if addon not in panels or not panels[addon][ATTR_ENABLE]:
|
||||
_LOGGER.error("Panel is not enable for %s", addon)
|
||||
if addon not in panels or not panels[addon].enable:
|
||||
_LOGGER.error("Panel is not enabled for %s", addon)
|
||||
return web.Response(status=HTTPStatus.BAD_REQUEST)
|
||||
data = panels[addon]
|
||||
|
||||
# Register panel
|
||||
_register_panel(self.hass, addon, data)
|
||||
_register_panel(self.hass, addon, panels[addon])
|
||||
return web.Response()
|
||||
|
||||
async def delete(self, request: web.Request, addon: str) -> web.Response:
|
||||
@@ -66,24 +61,23 @@ class HassIOAddonPanel(HomeAssistantView):
|
||||
frontend.async_remove_panel(self.hass, addon)
|
||||
return web.Response()
|
||||
|
||||
async def get_panels(self) -> dict:
|
||||
async def get_panels(self) -> dict[str, IngressPanel]:
|
||||
"""Return panels add-on info data."""
|
||||
try:
|
||||
data = await self.hassio.get_ingress_panels()
|
||||
return data[ATTR_PANELS]
|
||||
except HassioAPIError as err:
|
||||
return await self.client.ingress.panels()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.error("Can't read panel info: %s", err)
|
||||
return {}
|
||||
|
||||
|
||||
def _register_panel(hass: HomeAssistant, addon: str, data: dict[str, Any]):
|
||||
"""Init coroutine to register the panel."""
|
||||
def _register_panel(hass: HomeAssistant, addon: str, data: IngressPanel):
|
||||
"""Helper to register the panel."""
|
||||
frontend.async_register_built_in_panel(
|
||||
hass,
|
||||
"app",
|
||||
frontend_url_path=addon,
|
||||
sidebar_title=data[ATTR_TITLE],
|
||||
sidebar_icon=data[ATTR_ICON],
|
||||
require_admin=data[ATTR_ADMIN],
|
||||
sidebar_title=data.title,
|
||||
sidebar_icon=data.icon,
|
||||
require_admin=data.admin,
|
||||
config={"addon": addon},
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import os
|
||||
@@ -28,21 +27,6 @@ class HassioAPIError(RuntimeError):
|
||||
"""Return if a API trow a error."""
|
||||
|
||||
|
||||
def api_data[**_P](
|
||||
funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
|
||||
) -> Callable[_P, Coroutine[Any, Any, Any]]:
|
||||
"""Return data of an api."""
|
||||
|
||||
async def _wrapper(*argv: _P.args, **kwargs: _P.kwargs) -> Any:
|
||||
"""Wrap function."""
|
||||
data = await funct(*argv, **kwargs)
|
||||
if data["result"] == "ok":
|
||||
return data["data"]
|
||||
raise HassioAPIError(data["message"])
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
class HassIO:
|
||||
"""Small API wrapper for Hass.io."""
|
||||
|
||||
@@ -64,14 +48,6 @@ class HassIO:
|
||||
"""Return base url for Supervisor."""
|
||||
return self._base_url
|
||||
|
||||
@api_data
|
||||
def get_ingress_panels(self) -> Coroutine:
|
||||
"""Return data for Add-on ingress panels.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command("/ingress/panels", method="get")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str,
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -11,7 +11,8 @@ set_mode:
|
||||
required: true
|
||||
example: "away"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: mode
|
||||
|
||||
set_humidity:
|
||||
target:
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
started_drying: *trigger_common
|
||||
started_humidifying: *trigger_common
|
||||
|
||||
@@ -34,8 +34,10 @@ is_value:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_docked: *condition_common
|
||||
is_encountering_an_error: *condition_common
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
docked: *trigger_common
|
||||
errored: *trigger_common
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -225,7 +225,8 @@ turn_on:
|
||||
supported_features:
|
||||
- light.LightEntityFeature.EFFECT
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: effect
|
||||
advanced_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylitterbot==2025.1.0"]
|
||||
"requirements": ["pylitterbot==2025.2.0"]
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_jammed: *condition_common
|
||||
is_locked: *condition_common
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
jammed: *trigger_common
|
||||
locked: *trigger_common
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -7,6 +7,9 @@ stopped_playing:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_detected:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
detected:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_detected:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
detected:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_home: *condition_common
|
||||
is_not_home: *condition_common
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
entered_home: *trigger_common
|
||||
left_home: *trigger_common
|
||||
|
||||
@@ -158,6 +158,24 @@ BINARY_SENSOR_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = (
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="circulation_pump_status",
|
||||
translation_key="circulation_pump_status",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="power_on_delay_status",
|
||||
translation_key="power_on_delay_status",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="flow_delay_status",
|
||||
translation_key="flow_delay_status",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,12 @@
|
||||
"on": "mdi:thermometer-alert"
|
||||
}
|
||||
},
|
||||
"circulation_pump_status": {
|
||||
"default": "mdi:pump"
|
||||
},
|
||||
"flow_delay_status": {
|
||||
"default": "mdi:timer-sand"
|
||||
},
|
||||
"flow_rate_alarm": {
|
||||
"default": "mdi:autorenew",
|
||||
"state": {
|
||||
@@ -103,6 +109,9 @@
|
||||
"on": "mdi:flask-empty"
|
||||
}
|
||||
},
|
||||
"power_on_delay_status": {
|
||||
"default": "mdi:timer-sand"
|
||||
},
|
||||
"pump_alarm": {
|
||||
"default": "mdi:pump",
|
||||
"state": {
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
"alarm_water_too_hot": {
|
||||
"name": "Water too hot"
|
||||
},
|
||||
"circulation_pump_status": {
|
||||
"name": "Circulation pump"
|
||||
},
|
||||
"flow_delay_status": {
|
||||
"name": "Flow delay"
|
||||
},
|
||||
"flow_rate_alarm": {
|
||||
"name": "Flow rate"
|
||||
},
|
||||
@@ -95,6 +101,9 @@
|
||||
"ph_level_alarm": {
|
||||
"name": "pH tank level"
|
||||
},
|
||||
"power_on_delay_status": {
|
||||
"name": "Power-on delay"
|
||||
},
|
||||
"pump_alarm": {
|
||||
"name": "Recirculation"
|
||||
},
|
||||
|
||||
@@ -51,9 +51,11 @@ is_value:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
above: *number_or_entity_power
|
||||
below: *number_or_entity_power
|
||||
unit: *condition_unit_power
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
|
||||
@@ -15,9 +15,21 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import NODE_ONLINE, VM_CONTAINER_RUNNING
|
||||
from .const import (
|
||||
NODE_ONLINE,
|
||||
STATUS_OK,
|
||||
STORAGE_ACTIVE,
|
||||
STORAGE_ENABLED,
|
||||
STORAGE_SHARED,
|
||||
VM_CONTAINER_RUNNING,
|
||||
)
|
||||
from .coordinator import ProxmoxConfigEntry, ProxmoxNodeData
|
||||
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
||||
from .entity import (
|
||||
ProxmoxContainerEntity,
|
||||
ProxmoxNodeEntity,
|
||||
ProxmoxStorageEntity,
|
||||
ProxmoxVMEntity,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -43,6 +55,13 @@ class ProxmoxNodeBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
state_fn: Callable[[ProxmoxNodeData], bool | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ProxmoxStorageBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Class to hold Proxmox storage binary sensor description."""
|
||||
|
||||
state_fn: Callable[[dict[str, Any]], bool | None]
|
||||
|
||||
|
||||
NODE_SENSORS: tuple[ProxmoxNodeBinarySensorEntityDescription, ...] = (
|
||||
ProxmoxNodeBinarySensorEntityDescription(
|
||||
key="status",
|
||||
@@ -51,6 +70,15 @@ NODE_SENSORS: tuple[ProxmoxNodeBinarySensorEntityDescription, ...] = (
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
ProxmoxNodeBinarySensorEntityDescription(
|
||||
key="node_backup_status",
|
||||
translation_key="node_backup_status",
|
||||
state_fn=lambda data: bool(
|
||||
data.backups and data.backups[0]["status"] != STATUS_OK
|
||||
),
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
CONTAINER_SENSORS: tuple[ProxmoxContainerBinarySensorEntityDescription, ...] = (
|
||||
@@ -73,6 +101,27 @@ VM_SENSORS: tuple[ProxmoxVMBinarySensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
STORAGE_SENSORS: tuple[ProxmoxStorageBinarySensorEntityDescription, ...] = (
|
||||
ProxmoxStorageBinarySensorEntityDescription(
|
||||
key="storage_active",
|
||||
translation_key="storage_active",
|
||||
state_fn=lambda data: data["active"] == STORAGE_ACTIVE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
ProxmoxStorageBinarySensorEntityDescription(
|
||||
key="storage_enabled",
|
||||
translation_key="storage_enabled",
|
||||
state_fn=lambda data: data["enabled"] == STORAGE_ENABLED,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
ProxmoxStorageBinarySensorEntityDescription(
|
||||
key="storage_shared",
|
||||
translation_key="storage_shared",
|
||||
state_fn=lambda data: data["shared"] == STORAGE_SHARED,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -112,9 +161,22 @@ async def async_setup_entry(
|
||||
for entity_description in CONTAINER_SENSORS
|
||||
)
|
||||
|
||||
def _async_add_new_storages(
|
||||
storages: list[tuple[ProxmoxNodeData, dict[str, Any]]],
|
||||
) -> None:
|
||||
"""Add new storage binary sensors."""
|
||||
async_add_entities(
|
||||
ProxmoxStorageBinarySensor(
|
||||
coordinator, entity_description, storage, node_data
|
||||
)
|
||||
for (node_data, storage) in storages
|
||||
for entity_description in STORAGE_SENSORS
|
||||
)
|
||||
|
||||
coordinator.new_nodes_callbacks.append(_async_add_new_nodes)
|
||||
coordinator.new_vms_callbacks.append(_async_add_new_vms)
|
||||
coordinator.new_containers_callbacks.append(_async_add_new_containers)
|
||||
coordinator.new_storages_callbacks.append(_async_add_new_storages)
|
||||
|
||||
_async_add_new_nodes(
|
||||
[
|
||||
@@ -139,6 +201,14 @@ async def async_setup_entry(
|
||||
if (node_data.node["node"], vmid) in coordinator.known_containers
|
||||
]
|
||||
)
|
||||
_async_add_new_storages(
|
||||
[
|
||||
(node_data, storage_data)
|
||||
for node_data in coordinator.data.values()
|
||||
for storage_id, storage_data in node_data.storages.items()
|
||||
if (node_data.node["node"], storage_id) in coordinator.known_storages
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ProxmoxNodeBinarySensor(ProxmoxNodeEntity, BinarySensorEntity):
|
||||
@@ -172,3 +242,14 @@ class ProxmoxContainerBinarySensor(ProxmoxContainerEntity, BinarySensorEntity):
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.entity_description.state_fn(self.container_data)
|
||||
|
||||
|
||||
class ProxmoxStorageBinarySensor(ProxmoxStorageEntity, BinarySensorEntity):
|
||||
"""Representation of a Proxmox Storage binary sensor."""
|
||||
|
||||
entity_description: ProxmoxStorageBinarySensorEntityDescription
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.entity_description.state_fn(self.storage_data)
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
|
||||
@@ -141,6 +142,22 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
ProxmoxVMButtonEntityDescription(
|
||||
key="snapshot_create",
|
||||
translation_key="snapshot_create",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node)
|
||||
.qemu(vmid)
|
||||
.snapshot.post(
|
||||
name=(
|
||||
"homeassistant_snapshot_"
|
||||
f"{coordinator.data[node].vms[vmid]['name']}_"
|
||||
f"{dt_util.utcnow().strftime('%Y%m%d%H%M%S')}"
|
||||
)
|
||||
)
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
@@ -168,6 +185,22 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
),
|
||||
ProxmoxContainerButtonEntityDescription(
|
||||
key="snapshot_create",
|
||||
translation_key="snapshot_create",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node)
|
||||
.lxc(vmid)
|
||||
.snapshot.post(
|
||||
name=(
|
||||
"homeassistant_snapshot_"
|
||||
f"{coordinator.data[node].containers[vmid]['name']}_"
|
||||
f"{dt_util.utcnow().strftime('%Y%m%d%H%M%S')}"
|
||||
)
|
||||
)
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,9 +11,16 @@ CONF_TOKEN_SECRET = "token_value"
|
||||
CONF_VMS = "vms"
|
||||
CONF_CONTAINERS = "containers"
|
||||
|
||||
CONF_USER = "user"
|
||||
|
||||
NODE_ONLINE = "online"
|
||||
VM_CONTAINER_RUNNING = "running"
|
||||
|
||||
STORAGE_ACTIVE = 1
|
||||
STORAGE_SHARED = 1
|
||||
STORAGE_ENABLED = 1
|
||||
STATUS_OK = "ok"
|
||||
|
||||
AUTH_PAM = "pam"
|
||||
AUTH_PVE = "pve"
|
||||
AUTH_OTHER = "other"
|
||||
|
||||
@@ -37,6 +37,7 @@ from .const import (
|
||||
CONF_TOKEN_SECRET,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
DOMAIN,
|
||||
NODE_ONLINE,
|
||||
)
|
||||
|
||||
type ProxmoxConfigEntry = ConfigEntry[ProxmoxCoordinator]
|
||||
@@ -53,6 +54,8 @@ class ProxmoxNodeData:
|
||||
node: dict[str, Any] = field(default_factory=dict)
|
||||
vms: dict[int, dict[str, Any]] = field(default_factory=dict)
|
||||
containers: dict[int, dict[str, Any]] = field(default_factory=dict)
|
||||
storages: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
backups: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
@@ -78,6 +81,7 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
self.known_nodes: set[str] = set()
|
||||
self.known_vms: set[tuple[str, int]] = set()
|
||||
self.known_containers: set[tuple[str, int]] = set()
|
||||
self.known_storages: set[tuple[str, str]] = set()
|
||||
self.permissions: dict[str, dict[str, int]] = {}
|
||||
|
||||
self.new_nodes_callbacks: list[Callable[[list[ProxmoxNodeData]], None]] = []
|
||||
@@ -87,6 +91,9 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
self.new_containers_callbacks: list[
|
||||
Callable[[list[tuple[ProxmoxNodeData, dict[str, Any]]]], None]
|
||||
] = []
|
||||
self.new_storages_callbacks: list[
|
||||
Callable[[list[tuple[ProxmoxNodeData, dict[str, Any]]]], None]
|
||||
] = []
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
@@ -171,13 +178,17 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
) from err
|
||||
|
||||
data: dict[str, ProxmoxNodeData] = {}
|
||||
for node, (vms, containers) in zip(nodes, vms_containers, strict=True):
|
||||
for node, (vms, containers, storages, backups) in zip(
|
||||
nodes, vms_containers, strict=True
|
||||
):
|
||||
data[node[CONF_NODE]] = ProxmoxNodeData(
|
||||
node=node,
|
||||
vms={int(vm["vmid"]): vm for vm in vms},
|
||||
containers={
|
||||
int(container["vmid"]): container for container in containers
|
||||
},
|
||||
storages={s["storage"]: s for s in storages},
|
||||
backups=backups,
|
||||
)
|
||||
|
||||
self._async_add_remove_nodes(data)
|
||||
@@ -225,21 +236,47 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
def _fetch_all_nodes(
|
||||
self,
|
||||
) -> tuple[
|
||||
list[dict[str, Any]], list[tuple[list[dict[str, Any]], list[dict[str, Any]]]]
|
||||
list[dict[str, Any]],
|
||||
list[
|
||||
tuple[
|
||||
list[dict[str, Any]],
|
||||
list[dict[str, Any]],
|
||||
list[dict[str, Any]],
|
||||
list[dict[str, Any]],
|
||||
]
|
||||
],
|
||||
]:
|
||||
"""Fetch all nodes, and then proceed to the VMs and containers."""
|
||||
"""Fetch all nodes, and then proceed to the VMs, containers, storages, and backups."""
|
||||
nodes = self.proxmox.nodes.get() or []
|
||||
vms_containers = [self._get_vms_containers(node) for node in nodes]
|
||||
return nodes, vms_containers
|
||||
node_data = [self._get_node_data(node) for node in nodes]
|
||||
return nodes, node_data
|
||||
|
||||
def _get_vms_containers(
|
||||
def _get_node_data(
|
||||
self,
|
||||
node: dict[str, Any],
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""Get vms and containers for a node."""
|
||||
) -> tuple[
|
||||
list[dict[str, Any]],
|
||||
list[dict[str, Any]],
|
||||
list[dict[str, Any]],
|
||||
list[dict[str, Any]],
|
||||
]:
|
||||
"""Get vms, containers, storages, and backups for a node."""
|
||||
if node.get("status") != NODE_ONLINE:
|
||||
_LOGGER.debug(
|
||||
"Node %s is offline, skipping VM/container/storage fetch",
|
||||
node[CONF_NODE],
|
||||
)
|
||||
return [], [], [], []
|
||||
|
||||
vms = self.proxmox.nodes(node[CONF_NODE]).qemu.get() or []
|
||||
containers = self.proxmox.nodes(node[CONF_NODE]).lxc.get() or []
|
||||
return vms, containers
|
||||
storages = self.proxmox.nodes(node[CONF_NODE]).storage.get() or []
|
||||
backups = (
|
||||
self.proxmox.nodes(node[CONF_NODE]).tasks.get(typefilter="vzdump", limit=1)
|
||||
or []
|
||||
)
|
||||
|
||||
return vms, containers, storages, backups
|
||||
|
||||
def _async_add_remove_nodes(self, data: dict[str, ProxmoxNodeData]) -> None:
|
||||
"""Add new nodes/VMs/containers, track removals."""
|
||||
@@ -288,6 +325,23 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
for containers_callback in self.new_containers_callbacks:
|
||||
containers_callback(new_container_data)
|
||||
|
||||
current_storages = {
|
||||
(node_name, storage_name)
|
||||
for node_name, node_data in data.items()
|
||||
for storage_name in node_data.storages
|
||||
}
|
||||
self.known_storages &= current_storages
|
||||
new_storages = current_storages - self.known_storages
|
||||
if new_storages:
|
||||
_LOGGER.debug("New storages found: %s", new_storages)
|
||||
self.known_storages.update(new_storages)
|
||||
new_storage_data = [
|
||||
(data[node_name], data[node_name].storages[storage_name])
|
||||
for node_name, storage_name in new_storages
|
||||
]
|
||||
for storages_callback in self.new_storages_callbacks:
|
||||
storages_callback(new_storage_data)
|
||||
|
||||
|
||||
class ProxmoxSetupError(Exception):
|
||||
"""Base exception for Proxmox setup issues."""
|
||||
|
||||
@@ -10,9 +10,9 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import ProxmoxConfigEntry
|
||||
from .const import CONF_TOKEN_SECRET
|
||||
from .const import CONF_TOKEN_SECRET, CONF_USER
|
||||
|
||||
TO_REDACT = [CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_TOKEN_SECRET]
|
||||
TO_REDACT = [CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_TOKEN_SECRET, CONF_USER]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
|
||||
@@ -65,6 +65,59 @@ class ProxmoxNodeEntity(ProxmoxCoordinatorEntity):
|
||||
return super().available and self.device_name in self.coordinator.data
|
||||
|
||||
|
||||
class ProxmoxStorageEntity(ProxmoxCoordinatorEntity):
|
||||
"""Represents a Storage entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ProxmoxCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
storage_data: dict[str, Any],
|
||||
node_data: ProxmoxNodeData,
|
||||
) -> None:
|
||||
"""Initialize the Proxmox Storage entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._storage_data = storage_data
|
||||
self._node_name = node_data.node["node"]
|
||||
self.device_id = storage_data["storage"]
|
||||
self.device_name = storage_data["storage"]
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
f"{coordinator.config_entry.entry_id}_storage_{self.device_id}",
|
||||
)
|
||||
},
|
||||
name=f"Storage ({self.device_name})",
|
||||
model="Storage",
|
||||
configuration_url=_proxmox_base_url(coordinator).with_fragment(
|
||||
f"v1:0:=storage/{self._node_name}/{storage_data['storage']}"
|
||||
),
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
f"{coordinator.config_entry.entry_id}_node_{node_data.node['id']}",
|
||||
),
|
||||
)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self._node_name}_{self.device_id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return (
|
||||
super().available
|
||||
and self._node_name in self.coordinator.data
|
||||
and self.device_id in self.coordinator.data[self._node_name].storages
|
||||
)
|
||||
|
||||
@property
|
||||
def storage_data(self) -> dict[str, Any]:
|
||||
"""Return the Storage data."""
|
||||
return self.coordinator.data[self._node_name].storages[self.device_id]
|
||||
|
||||
|
||||
class ProxmoxVMEntity(ProxmoxCoordinatorEntity):
|
||||
"""Represents a VM entity."""
|
||||
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"storage_active": {
|
||||
"default": "mdi:play",
|
||||
"state": {
|
||||
"off": "mdi:stop"
|
||||
}
|
||||
},
|
||||
"storage_enabled": {
|
||||
"default": "mdi:power",
|
||||
"state": {
|
||||
"off": "mdi:power-off"
|
||||
}
|
||||
},
|
||||
"storage_shared": {
|
||||
"default": "mdi:lan-connect",
|
||||
"state": {
|
||||
"off": "mdi:lan-disconnect"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"hibernate": {
|
||||
"default": "mdi:power-sleep"
|
||||
@@ -10,6 +30,9 @@
|
||||
"shutdown": {
|
||||
"default": "mdi:power"
|
||||
},
|
||||
"snapshot_create": {
|
||||
"default": "mdi:backup-restore"
|
||||
},
|
||||
"start": {
|
||||
"default": "mdi:play"
|
||||
},
|
||||
@@ -81,6 +104,18 @@
|
||||
"node_status": {
|
||||
"default": "mdi:server"
|
||||
},
|
||||
"storage_available": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"storage_total": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"storage_used": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"storage_used_percentage": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"vm_cpu": {
|
||||
"default": "mdi:cpu-64-bit"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -17,9 +18,15 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.const import PERCENTAGE, UnitOfInformation, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .coordinator import ProxmoxConfigEntry, ProxmoxNodeData
|
||||
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
||||
from .entity import (
|
||||
ProxmoxContainerEntity,
|
||||
ProxmoxNodeEntity,
|
||||
ProxmoxStorageEntity,
|
||||
ProxmoxVMEntity,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -28,7 +35,7 @@ PARALLEL_UPDATES = 0
|
||||
class ProxmoxNodeSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class to hold Proxmox node sensor description."""
|
||||
|
||||
value_fn: Callable[[ProxmoxNodeData], StateType]
|
||||
value_fn: Callable[[ProxmoxNodeData], StateType | datetime]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -45,6 +52,13 @@ class ProxmoxContainerSensorEntityDescription(SensorEntityDescription):
|
||||
value_fn: Callable[[dict[str, Any]], StateType]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ProxmoxStorageSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class to hold Proxmox storage sensor description."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], StateType]
|
||||
|
||||
|
||||
NODE_SENSORS: tuple[ProxmoxNodeSensorEntityDescription, ...] = (
|
||||
ProxmoxNodeSensorEntityDescription(
|
||||
key="node_cpu",
|
||||
@@ -130,6 +144,31 @@ NODE_SENSORS: tuple[ProxmoxNodeSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["online", "offline"],
|
||||
),
|
||||
ProxmoxNodeSensorEntityDescription(
|
||||
key="node_backup_last_backup",
|
||||
translation_key="node_backup_last_backup",
|
||||
value_fn=lambda data: (
|
||||
dt_util.utc_from_timestamp(data.backups[0]["endtime"])
|
||||
if data.backups
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
ProxmoxNodeSensorEntityDescription(
|
||||
key="node_backup_duration",
|
||||
translation_key="node_backup_duration",
|
||||
value_fn=lambda data: (
|
||||
data.backups[0]["endtime"] - data.backups[0]["starttime"]
|
||||
if data.backups
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
VM_SENSORS: tuple[ProxmoxVMSensorEntityDescription, ...] = (
|
||||
@@ -350,6 +389,50 @@ CONTAINER_SENSORS: tuple[ProxmoxContainerSensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
STORAGE_SENSORS: tuple[ProxmoxStorageSensorEntityDescription, ...] = (
|
||||
ProxmoxStorageSensorEntityDescription(
|
||||
key="storage_used",
|
||||
translation_key="storage_used",
|
||||
value_fn=lambda data: data["used"],
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ProxmoxStorageSensorEntityDescription(
|
||||
key="storage_total",
|
||||
translation_key="storage_total",
|
||||
value_fn=lambda data: data["total"],
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ProxmoxStorageSensorEntityDescription(
|
||||
key="storage_available",
|
||||
translation_key="storage_available",
|
||||
value_fn=lambda data: data["avail"],
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ProxmoxStorageSensorEntityDescription(
|
||||
key="storage_used_percentage",
|
||||
translation_key="storage_used_percentage",
|
||||
value_fn=lambda data: round(data["used_fraction"] * 100, 1),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -389,9 +472,20 @@ async def async_setup_entry(
|
||||
for entity_description in CONTAINER_SENSORS
|
||||
)
|
||||
|
||||
def _async_add_new_storages(
|
||||
storages: list[tuple[ProxmoxNodeData, dict[str, Any]]],
|
||||
) -> None:
|
||||
"""Add new storage sensors."""
|
||||
async_add_entities(
|
||||
ProxmoxStorageSensor(coordinator, entity_description, storage, node_data)
|
||||
for (node_data, storage) in storages
|
||||
for entity_description in STORAGE_SENSORS
|
||||
)
|
||||
|
||||
coordinator.new_nodes_callbacks.append(_async_add_new_nodes)
|
||||
coordinator.new_vms_callbacks.append(_async_add_new_vms)
|
||||
coordinator.new_containers_callbacks.append(_async_add_new_containers)
|
||||
coordinator.new_storages_callbacks.append(_async_add_new_storages)
|
||||
|
||||
_async_add_new_nodes(
|
||||
[
|
||||
@@ -416,6 +510,14 @@ async def async_setup_entry(
|
||||
if (node_data.node["node"], vmid) in coordinator.known_containers
|
||||
]
|
||||
)
|
||||
_async_add_new_storages(
|
||||
[
|
||||
(node_data, storage_data)
|
||||
for node_data in coordinator.data.values()
|
||||
for storage_name, storage_data in node_data.storages.items()
|
||||
if (node_data.node["node"], storage_name) in coordinator.known_storages
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ProxmoxNodeSensor(ProxmoxNodeEntity, SensorEntity):
|
||||
@@ -424,7 +526,7 @@ class ProxmoxNodeSensor(ProxmoxNodeEntity, SensorEntity):
|
||||
entity_description: ProxmoxNodeSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the native value of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data[self.device_name])
|
||||
|
||||
@@ -449,3 +551,14 @@ class ProxmoxContainerSensor(ProxmoxContainerEntity, SensorEntity):
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the native value of the sensor."""
|
||||
return self.entity_description.value_fn(self.container_data)
|
||||
|
||||
|
||||
class ProxmoxStorageSensor(ProxmoxStorageEntity, SensorEntity):
|
||||
"""Represents a Proxmox VE storage sensor."""
|
||||
|
||||
entity_description: ProxmoxStorageSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the native value of the sensor."""
|
||||
return self.entity_description.value_fn(self.storage_data)
|
||||
|
||||
@@ -105,8 +105,32 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"node_backup_status": {
|
||||
"name": "Backup status"
|
||||
},
|
||||
"status": {
|
||||
"name": "Status"
|
||||
},
|
||||
"storage_active": {
|
||||
"name": "Storage active",
|
||||
"state": {
|
||||
"off": "Inactive",
|
||||
"on": "Active"
|
||||
}
|
||||
},
|
||||
"storage_enabled": {
|
||||
"name": "Storage enabled",
|
||||
"state": {
|
||||
"off": "Disabled",
|
||||
"on": "Enabled"
|
||||
}
|
||||
},
|
||||
"storage_shared": {
|
||||
"name": "Storage shared",
|
||||
"state": {
|
||||
"off": "Not shared",
|
||||
"on": "Shared"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -119,6 +143,9 @@
|
||||
"shutdown": {
|
||||
"name": "Shutdown"
|
||||
},
|
||||
"snapshot_create": {
|
||||
"name": "Create snapshot"
|
||||
},
|
||||
"start": {
|
||||
"name": "Start"
|
||||
},
|
||||
@@ -174,6 +201,12 @@
|
||||
"container_uptime": {
|
||||
"name": "Uptime"
|
||||
},
|
||||
"node_backup_duration": {
|
||||
"name": "Backup duration"
|
||||
},
|
||||
"node_backup_last_backup": {
|
||||
"name": "Last backup"
|
||||
},
|
||||
"node_cpu": {
|
||||
"name": "CPU usage"
|
||||
},
|
||||
@@ -205,6 +238,18 @@
|
||||
"node_uptime": {
|
||||
"name": "Uptime"
|
||||
},
|
||||
"storage_available": {
|
||||
"name": "Available storage"
|
||||
},
|
||||
"storage_total": {
|
||||
"name": "Total storage"
|
||||
},
|
||||
"storage_used": {
|
||||
"name": "Used storage"
|
||||
},
|
||||
"storage_used_percentage": {
|
||||
"name": "Storage usage percentage"
|
||||
},
|
||||
"vm_cpu": {
|
||||
"name": "CPU usage"
|
||||
},
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["satel_integra"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["satel-integra==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ rules:
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: todo
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: This integration does not provide any service actions.
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -227,6 +227,25 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
},
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_DUST_BAG: {
|
||||
Attribute.STATUS: SmartThingsBinarySensorEntityDescription(
|
||||
key=Attribute.STATUS,
|
||||
is_on_key="full",
|
||||
component_translation_key={
|
||||
"station": "stick_cleaner_dust_bag",
|
||||
},
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
exists_fn=lambda component, _: component == "station",
|
||||
)
|
||||
},
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_STICK_STATUS: {
|
||||
Attribute.STATUS: SmartThingsBinarySensorEntityDescription(
|
||||
key=Attribute.STATUS,
|
||||
is_on_key="charging",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -135,6 +135,14 @@ HEALTH_CONCERN = {
|
||||
"hazardous": "hazardous",
|
||||
}
|
||||
|
||||
STICK_CLEANER_STATUS = {
|
||||
"ready": "ready",
|
||||
"usingVacuum": "using_vacuum",
|
||||
"emptyingDustbin": "emptying_dustbin",
|
||||
"UVCleaning": "uv_cleaning",
|
||||
"UVPaused": "uv_paused",
|
||||
}
|
||||
|
||||
WASHER_OPTIONS = ["pause", "run", "stop"]
|
||||
|
||||
|
||||
@@ -1239,6 +1247,39 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
],
|
||||
},
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_DUST_BAG: {
|
||||
Attribute.USAGE: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
key=Attribute.USAGE,
|
||||
translation_key="stick_cleaner_dust_bag_usage",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
component_fn=lambda component: component == "station",
|
||||
)
|
||||
]
|
||||
},
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_STATUS: {
|
||||
Attribute.OPERATING_STATE: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
key=Attribute.OPERATING_STATE,
|
||||
name=None,
|
||||
translation_key="stick_cleaner_operating_state",
|
||||
options=list(STICK_CLEANER_STATUS.values()),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
)
|
||||
]
|
||||
},
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_DUSTBIN_STATUS: {
|
||||
Attribute.LAST_EMPTIED_TIME: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
key=Attribute.LAST_EMPTIED_TIME,
|
||||
translation_key="stick_cleaner_dustbin_last_emptied",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
|
||||
)
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"robot_cleaner_dust_bag": {
|
||||
"name": "Dust bag full"
|
||||
},
|
||||
"stick_cleaner_dust_bag": {
|
||||
"name": "[%key:component::smartthings::entity::binary_sensor::robot_cleaner_dust_bag::name%]"
|
||||
},
|
||||
"stick_cleaner_status": {
|
||||
"name": "Stick cleaner in station"
|
||||
},
|
||||
@@ -836,6 +839,22 @@
|
||||
"tested": "[%key:component::smartthings::entity::sensor::carbon_monoxide_detector::state::tested%]"
|
||||
}
|
||||
},
|
||||
"stick_cleaner_dust_bag_usage": {
|
||||
"name": "Dust bag cycles",
|
||||
"unit_of_measurement": "cycles"
|
||||
},
|
||||
"stick_cleaner_dustbin_last_emptied": {
|
||||
"name": "Last emptied"
|
||||
},
|
||||
"stick_cleaner_operating_state": {
|
||||
"state": {
|
||||
"emptying_dustbin": "Emptying dustbin",
|
||||
"ready": "[%key:component::smartthings::entity::sensor::oven_machine_state::state::ready%]",
|
||||
"using_vacuum": "Using vacuum",
|
||||
"uv_cleaning": "UV cleaning",
|
||||
"uv_paused": "UV paused"
|
||||
}
|
||||
},
|
||||
"thermostat_cooling_setpoint": {
|
||||
"name": "Cooling setpoint"
|
||||
},
|
||||
@@ -965,6 +984,9 @@
|
||||
"dry_plus": {
|
||||
"name": "Dry plus"
|
||||
},
|
||||
"empty_dustbin": {
|
||||
"name": "Empty dustbin"
|
||||
},
|
||||
"heated_dry": {
|
||||
"name": "Heated dry"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,14 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio
|
||||
on_command=Command.ENABLE_SOUND_DETECTION,
|
||||
off_command=Command.DISABLE_SOUND_DETECTION,
|
||||
),
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_DUSTBIN_STATUS: SmartThingsSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STICK_CLEANER_DUSTBIN_STATUS,
|
||||
translation_key="empty_dustbin",
|
||||
status_attribute=Attribute.OPERATING_STATE,
|
||||
on_key="emptying",
|
||||
on_command=Command.START_EMPTYING,
|
||||
off_command=Command.STOP_EMPTYING,
|
||||
),
|
||||
}
|
||||
DISHWASHER_WASHING_OPTIONS_TO_SWITCHES: dict[
|
||||
Attribute | str, SmartThingsDishwasherWashingOptionSwitchEntityDescription
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
mode: condition
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
mode: trigger
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -35,6 +35,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
ENCRYPTED_MODELS,
|
||||
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL,
|
||||
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES,
|
||||
SupportedModels,
|
||||
)
|
||||
from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator
|
||||
@@ -261,7 +262,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
|
||||
sensor_type: str = entry.data[CONF_SENSOR_TYPE]
|
||||
switchbot_model = HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL[sensor_type]
|
||||
# connectable means we can make connections to the device
|
||||
connectable = switchbot_model in CONNECTABLE_SUPPORTED_MODEL_TYPES
|
||||
connectable = (
|
||||
switchbot_model in CONNECTABLE_SUPPORTED_MODEL_TYPES
|
||||
and switchbot_model not in NON_CONNECTABLE_SUPPORTED_MODEL_TYPES
|
||||
)
|
||||
address: str = entry.data[CONF_ADDRESS]
|
||||
|
||||
await switchbot.close_stale_connections_by_address(address)
|
||||
|
||||
@@ -113,8 +113,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if (
|
||||
not discovery_info.connectable
|
||||
and model_name in CONNECTABLE_SUPPORTED_MODEL_TYPES
|
||||
and model_name not in NON_CONNECTABLE_SUPPORTED_MODEL_TYPES
|
||||
):
|
||||
# Source is not connectable but the model is connectable
|
||||
# Source is not connectable but the model is connectable only
|
||||
return self.async_abort(reason="not_supported")
|
||||
self._discovered_adv = parsed
|
||||
data = parsed.data
|
||||
|
||||
@@ -117,6 +117,7 @@ NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||
SwitchbotModel.METER: SupportedModels.HYGROMETER,
|
||||
SwitchbotModel.IO_METER: SupportedModels.HYGROMETER,
|
||||
SwitchbotModel.METER_PRO: SupportedModels.HYGROMETER,
|
||||
SwitchbotModel.METER_PRO_C: SupportedModels.HYGROMETER_CO2,
|
||||
SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT,
|
||||
SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION,
|
||||
SwitchbotModel.PRESENCE_SENSOR: SupportedModels.PRESENCE_SENSOR,
|
||||
|
||||
@@ -69,6 +69,7 @@ from .const import (
|
||||
ATTR_IS_BIG,
|
||||
ATTR_KEYBOARD,
|
||||
ATTR_KEYBOARD_INLINE,
|
||||
ATTR_MEDIA,
|
||||
ATTR_MEDIA_TYPE,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_MESSAGE_ID,
|
||||
@@ -79,6 +80,7 @@ from .const import (
|
||||
ATTR_OPTIONS,
|
||||
ATTR_PARSER,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_PROTECT_CONTENT,
|
||||
ATTR_QUESTION,
|
||||
ATTR_REACTION,
|
||||
ATTR_REPLY_TO_MSGID,
|
||||
@@ -125,6 +127,7 @@ from .const import (
|
||||
SERVICE_SEND_CHAT_ACTION,
|
||||
SERVICE_SEND_DOCUMENT,
|
||||
SERVICE_SEND_LOCATION,
|
||||
SERVICE_SEND_MEDIA_GROUP,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
SERVICE_SEND_PHOTO,
|
||||
SERVICE_SEND_POLL,
|
||||
@@ -219,6 +222,43 @@ SERVICE_SCHEMA_SEND_FILE = vol.All(
|
||||
SERVICE_SCHEMA_BASE_SEND_FILE,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_MEDIA_GROUP = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
|
||||
vol.Optional(ATTR_CHAT_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Required(ATTR_MEDIA): vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MEDIA_TYPE): vol.In(
|
||||
(
|
||||
str(InputMediaType.AUDIO),
|
||||
str(InputMediaType.VIDEO),
|
||||
str(InputMediaType.DOCUMENT),
|
||||
str(InputMediaType.PHOTO),
|
||||
)
|
||||
),
|
||||
vol.Optional(ATTR_URL): cv.string,
|
||||
vol.Optional(ATTR_FILE): cv.string,
|
||||
vol.Optional(ATTR_CAPTION): cv.string,
|
||||
vol.Optional(ATTR_USERNAME): cv.string,
|
||||
vol.Optional(ATTR_PASSWORD): cv.string,
|
||||
vol.Optional(ATTR_AUTHENTICATION): cv.string,
|
||||
vol.Optional(ATTR_VERIFY_SSL, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
],
|
||||
vol.Length(min=2, max=10),
|
||||
),
|
||||
vol.Optional(ATTR_PARSER): ATTR_PARSER_SCHEMA,
|
||||
vol.Optional(ATTR_DISABLE_NOTIF): cv.boolean,
|
||||
vol.Optional(ATTR_PROTECT_CONTENT): cv.boolean,
|
||||
vol.Optional(ATTR_REPLY_TO_MSGID): vol.Coerce(int),
|
||||
vol.Optional(ATTR_MESSAGE_THREAD_ID): vol.Coerce(int),
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_STICKER = vol.All(
|
||||
cv.deprecated(ATTR_TIMEOUT),
|
||||
@@ -386,6 +426,7 @@ SERVICE_MAP: dict[str, VolSchemaType] = {
|
||||
SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE,
|
||||
SERVICE_SEND_CHAT_ACTION: SERVICE_SCHEMA_SEND_CHAT_ACTION,
|
||||
SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE,
|
||||
SERVICE_SEND_MEDIA_GROUP: SERVICE_SCHEMA_SEND_MEDIA_GROUP,
|
||||
SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_STICKER,
|
||||
SERVICE_SEND_ANIMATION: SERVICE_SCHEMA_SEND_FILE,
|
||||
SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE,
|
||||
@@ -434,6 +475,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
SERVICE_SEND_MESSAGE,
|
||||
SERVICE_SEND_CHAT_ACTION,
|
||||
SERVICE_SEND_PHOTO,
|
||||
SERVICE_SEND_MEDIA_GROUP,
|
||||
SERVICE_SEND_ANIMATION,
|
||||
SERVICE_SEND_VIDEO,
|
||||
SERVICE_SEND_VOICE,
|
||||
@@ -539,6 +581,10 @@ async def _call_service(
|
||||
messages: dict[str, JsonValueType] | None = None
|
||||
if service_name == SERVICE_SEND_MESSAGE:
|
||||
messages = await notify_service.send_message(context=service.context, **kwargs)
|
||||
elif service_name == SERVICE_SEND_MEDIA_GROUP:
|
||||
messages = await notify_service.send_media_group(
|
||||
context=service.context, **kwargs
|
||||
)
|
||||
elif service_name == SERVICE_SEND_CHAT_ACTION:
|
||||
messages = await notify_service.send_chat_action(
|
||||
context=service.context, **kwargs
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user