mirror of
https://github.com/home-assistant/core.git
synced 2026-01-12 02:27:18 +01:00
Compare commits
91 Commits
power-sens
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f4063f91e | ||
|
|
080ba46885 | ||
|
|
2cb028ee79 | ||
|
|
72655dbf0b | ||
|
|
153278221d | ||
|
|
4942ce7e86 | ||
|
|
98e918cd8a | ||
|
|
1efc87bfef | ||
|
|
b4360ccbd9 | ||
|
|
ce234d69a7 | ||
|
|
b2a198e230 | ||
|
|
538009d2df | ||
|
|
99329851a2 | ||
|
|
f8ec395e96 | ||
|
|
98fe189edf | ||
|
|
7b413e3fd3 | ||
|
|
00ca5473d4 | ||
|
|
33c808713e | ||
|
|
c97437fbf3 | ||
|
|
ad8f14fec1 | ||
|
|
7df586eff1 | ||
|
|
f6fa95d2f7 | ||
|
|
23a8300012 | ||
|
|
694d67d2d5 | ||
|
|
a26c910db7 | ||
|
|
ac9d04624b | ||
|
|
a0ec7bde33 | ||
|
|
5f7dc49215 | ||
|
|
f79eef150e | ||
|
|
1733599442 | ||
|
|
3bde4f606b | ||
|
|
afb635125c | ||
|
|
876d54ad4d | ||
|
|
c20cd8fb94 | ||
|
|
e15b2ec0cb | ||
|
|
1829452ef1 | ||
|
|
9d8dc9ec06 | ||
|
|
72a3523193 | ||
|
|
7c3541e983 | ||
|
|
8246fc78fa | ||
|
|
78dd3aee10 | ||
|
|
c22e578aca | ||
|
|
1021c1959e | ||
|
|
d3161d8e92 | ||
|
|
fc468b56c8 | ||
|
|
ea48dc3c58 | ||
|
|
11dde08d79 | ||
|
|
5e43708a40 | ||
|
|
1ac2280266 | ||
|
|
6b1ad8d2d1 | ||
|
|
c1741237f4 | ||
|
|
8ecacd6490 | ||
|
|
188ab3930c | ||
|
|
a8dba53185 | ||
|
|
a2ef0c9a75 | ||
|
|
5a1fe17580 | ||
|
|
34388f52a6 | ||
|
|
fc2199fcf7 | ||
|
|
2236f8cd07 | ||
|
|
8d376027bf | ||
|
|
47e91bc2ec | ||
|
|
33d1cdd0ac | ||
|
|
f46de054ba | ||
|
|
741aa714dd | ||
|
|
5fac7d4ffb | ||
|
|
341c441e61 | ||
|
|
a1edf0a77c | ||
|
|
dd84b52c7b | ||
|
|
43ced677e5 | ||
|
|
7a696935ed | ||
|
|
be3be360a7 | ||
|
|
092ebaaeb1 | ||
|
|
e8025317ed | ||
|
|
39b025dfea | ||
|
|
1b436a8808 | ||
|
|
a7440e3756 | ||
|
|
2c7852f94b | ||
|
|
bd4653f830 | ||
|
|
c0b2847a87 | ||
|
|
8853f6698b | ||
|
|
b1a3ad6ac3 | ||
|
|
dafa2e69e2 | ||
|
|
2c6d6f8ab4 | ||
|
|
10d32b7f23 | ||
|
|
e4dc4e0ced | ||
|
|
6f9794f235 | ||
|
|
b8cff13737 | ||
|
|
7777714cc0 | ||
|
|
f15d5cdf2a | ||
|
|
6181f4e7de | ||
|
|
80df3b5b80 |
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -661,6 +661,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/hassio/ @home-assistant/supervisor
|
||||
/tests/components/hassio/ @home-assistant/supervisor
|
||||
/homeassistant/components/hdfury/ @glenndehaan
|
||||
/tests/components/hdfury/ @glenndehaan
|
||||
/homeassistant/components/hdmi_cec/ @inytar
|
||||
/tests/components/hdmi_cec/ @inytar
|
||||
/homeassistant/components/heatmiser/ @andylockran
|
||||
@@ -1170,8 +1172,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/open_router/ @joostlek
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
/tests/components/openerz/ @misialq
|
||||
/homeassistant/components/openevse/ @c00w
|
||||
/tests/components/openevse/ @c00w
|
||||
/homeassistant/components/openevse/ @c00w @firstof9
|
||||
/tests/components/openevse/ @c00w @firstof9
|
||||
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
||||
/tests/components/openexchangerates/ @MartinHjelmare
|
||||
/homeassistant/components/opengarage/ @danielhiversen
|
||||
|
||||
@@ -43,6 +43,13 @@ BUTTON_TYPES: tuple[AirobotButtonEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda coordinator: coordinator.client.reboot_thermostat(),
|
||||
),
|
||||
AirobotButtonEntityDescription(
|
||||
key="recalibrate_co2",
|
||||
translation_key="recalibrate_co2",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
press_fn=lambda coordinator: coordinator.client.recalibrate_co2_sensor(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"recalibrate_co2": {
|
||||
"default": "mdi:molecule-co2"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"hysteresis_band": {
|
||||
"default": "mdi:delta"
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairobotrest"],
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pyairobotrest==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
|
||||
@@ -59,6 +59,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"recalibrate_co2": {
|
||||
"name": "Recalibrate CO2 sensor"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"hysteresis_band": {
|
||||
"name": "Hysteresis band"
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==1.0.4"]
|
||||
"requirements": ["aioairzone==1.0.5"]
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, llm
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import AnthropicConfigEntry
|
||||
@@ -193,7 +194,7 @@ def _convert_content(
|
||||
tool_result_block = ToolResultBlockParam(
|
||||
type="tool_result",
|
||||
tool_use_id=content.tool_call_id,
|
||||
content=json.dumps(content.tool_result),
|
||||
content=json_dumps(content.tool_result),
|
||||
)
|
||||
external_tool = False
|
||||
if not messages or messages[-1]["role"] != (
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
|
||||
"requirements": ["pysilero-vad==3.2.0", "pyspeex-noise==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Cache TTL for backup list (in seconds)
|
||||
CACHE_TTL = 300
|
||||
|
||||
# Timeout for upload operations (in seconds)
|
||||
# This prevents uploads from hanging indefinitely
|
||||
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
|
||||
|
||||
|
||||
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata files."""
|
||||
@@ -329,13 +333,28 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug("Uploading backup file %s with streaming", filename)
|
||||
try:
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
file_version = await self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
file_version = await asyncio.wait_for(
|
||||
self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
),
|
||||
timeout=UPLOAD_TIMEOUT,
|
||||
)
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"Upload of %s timed out after %s seconds", filename, UPLOAD_TIMEOUT
|
||||
)
|
||||
reader.abort()
|
||||
raise BackupAgentError(
|
||||
f"Upload timed out after {UPLOAD_TIMEOUT} seconds"
|
||||
) from None
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.warning("Upload of %s was cancelled", filename)
|
||||
reader.abort()
|
||||
raise
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
|
||||
@@ -34,7 +34,12 @@ class BeoData:
|
||||
|
||||
type BeoConfigEntry = ConfigEntry[BeoData]
|
||||
|
||||
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.EVENT,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
||||
|
||||
63
homeassistant/components/bang_olufsen/binary_sensor.py
Normal file
63
homeassistant/components/bang_olufsen/binary_sensor.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Binary Sensor entities for the Bang & Olufsen integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mozart_api.models import BatteryState
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BeoConfigEntry
|
||||
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
|
||||
from .entity import BeoEntity
|
||||
from .util import supports_battery
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: BeoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Binary Sensor entities from config entry."""
|
||||
if await supports_battery(config_entry.runtime_data.client):
|
||||
async_add_entities(new_entities=[BeoBinarySensorBatteryCharging(config_entry)])
|
||||
|
||||
|
||||
class BeoBinarySensorBatteryCharging(BinarySensorEntity, BeoEntity):
|
||||
"""Battery charging Binary Sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
|
||||
_attr_is_on = False
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Init the battery charging Binary Sensor."""
|
||||
super().__init__(config_entry, config_entry.runtime_data.client)
|
||||
|
||||
self._attr_unique_id = f"{self._unique_id}_charging"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
self._update_battery_charging,
|
||||
)
|
||||
)
|
||||
|
||||
async def _update_battery_charging(self, data: BatteryState) -> None:
|
||||
"""Update battery charging."""
|
||||
self._attr_is_on = bool(data.is_charging)
|
||||
self.async_write_ha_state()
|
||||
@@ -115,6 +115,7 @@ class WebsocketNotification(StrEnum):
|
||||
"""Enum for WebSocket notification types."""
|
||||
|
||||
ACTIVE_LISTENING_MODE = "active_listening_mode"
|
||||
BATTERY = "battery"
|
||||
BEO_REMOTE_BUTTON = "beo_remote_button"
|
||||
BUTTON = "button"
|
||||
PLAYBACK_ERROR = "playback_error"
|
||||
|
||||
@@ -4,8 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -55,6 +57,19 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
# Get remotes
|
||||
for remote in await get_remotes(config_entry.runtime_data.client):
|
||||
# Get Battery Sensor states
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{remote.serial_number}_{config_entry.unique_id}_remote_battery_level",
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data[f"remote_{remote.serial_number}_battery_level"] = state_dict
|
||||
|
||||
# Get key Event entity states (if enabled)
|
||||
for key_type in get_remote_keys():
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
@@ -72,4 +87,26 @@ async def async_get_config_entry_diagnostics(
|
||||
# Add remote Mozart model
|
||||
data[f"remote_{remote.serial_number}"] = dict(remote)
|
||||
|
||||
# Get Mozart battery entity
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_battery_level"
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data["battery_level"] = state_dict
|
||||
|
||||
# Get Mozart battery charging entity
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_charging"
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data["charging"] = state_dict
|
||||
|
||||
return data
|
||||
|
||||
139
homeassistant/components/bang_olufsen/sensor.py
Normal file
139
homeassistant/components/bang_olufsen/sensor.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Sensor entities for the Bang & Olufsen integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from datetime import timedelta
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from mozart_api.exceptions import ApiException
|
||||
from mozart_api.models import BatteryState, PairedRemote
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BeoConfigEntry
|
||||
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
|
||||
from .entity import BeoEntity
|
||||
from .util import get_remotes, supports_battery
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: BeoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sensor entities from config entry."""
|
||||
entities: list[BeoSensor] = []
|
||||
|
||||
# Check for Mozart device with battery
|
||||
if await supports_battery(config_entry.runtime_data.client):
|
||||
entities.append(BeoSensorBatteryLevel(config_entry))
|
||||
|
||||
# Add any Beoremote One remotes
|
||||
entities.extend(
|
||||
[
|
||||
BeoSensorRemoteBatteryLevel(config_entry, remote)
|
||||
for remote in (await get_remotes(config_entry.runtime_data.client))
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
class BeoSensor(SensorEntity, BeoEntity):
|
||||
"""Base Bang & Olufsen Sensor."""
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Initialize Sensor."""
|
||||
super().__init__(config_entry, config_entry.runtime_data.client)
|
||||
|
||||
|
||||
class BeoSensorBatteryLevel(BeoSensor):
|
||||
"""Battery level Sensor for Mozart devices."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Init the battery level Sensor."""
|
||||
super().__init__(config_entry)
|
||||
|
||||
self._attr_unique_id = f"{self._unique_id}_battery_level"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
self._update_battery,
|
||||
)
|
||||
)
|
||||
|
||||
async def _update_battery(self, data: BatteryState) -> None:
|
||||
"""Update sensor value."""
|
||||
self._attr_native_value = data.battery_level
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class BeoSensorRemoteBatteryLevel(BeoSensor):
|
||||
"""Battery level Sensor for the Beoremote One."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_should_poll = True
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry, remote: PairedRemote) -> None:
|
||||
"""Init the battery level Sensor."""
|
||||
super().__init__(config_entry)
|
||||
# Serial number is not None, as the remote object is provided by get_remotes
|
||||
assert remote.serial_number
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{remote.serial_number}_{self._unique_id}_remote_battery_level"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")}
|
||||
)
|
||||
self._attr_native_value = remote.battery_level
|
||||
self._remote = remote
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Poll battery status."""
|
||||
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
|
||||
for remote in await get_remotes(self._client):
|
||||
if remote.serial_number == self._remote.serial_number:
|
||||
self._attr_native_value = remote.battery_level
|
||||
break
|
||||
@@ -84,3 +84,10 @@ def get_remote_keys() -> list[str]:
|
||||
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
async def supports_battery(client: MozartClient) -> bool:
|
||||
"""Get if a Mozart device has a battery."""
|
||||
battery_state = await client.get_battery_state()
|
||||
|
||||
return battery_state.state != "BatteryNotPresent"
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from mozart_api.models import (
|
||||
BatteryState,
|
||||
BeoRemoteButton,
|
||||
ButtonEvent,
|
||||
ListeningModeProps,
|
||||
@@ -60,6 +61,7 @@ class BeoWebsocket(BeoBase):
|
||||
self._client.get_active_listening_mode_notifications(
|
||||
self.on_active_listening_mode
|
||||
)
|
||||
self._client.get_battery_notifications(self.on_battery_notification)
|
||||
self._client.get_beo_remote_button_notifications(
|
||||
self.on_beo_remote_button_notification
|
||||
)
|
||||
@@ -115,6 +117,14 @@ class BeoWebsocket(BeoBase):
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_battery_notification(self, notification: BatteryState) -> None:
|
||||
"""Send battery dispatch."""
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_beo_remote_button_notification(self, notification: BeoRemoteButton) -> None:
|
||||
"""Send beo_remote_button dispatch."""
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.components.media_player import MediaType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -56,8 +56,31 @@ def catch_braviatv_errors[_BraviaTVCoordinatorT: BraviaTVCoordinator, **_P](
|
||||
"""Catch Bravia errors and log message."""
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except BraviaNotFound as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_error_not_found",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_error_offline",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except BraviaError as err:
|
||||
_LOGGER.error("Command error: %s", err)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_error",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
await self.async_request_refresh()
|
||||
|
||||
return wrapper
|
||||
@@ -165,17 +188,35 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
|
||||
if self.skipped_updates < 10:
|
||||
self.connected = False
|
||||
self.skipped_updates += 1
|
||||
_LOGGER.debug("Update skipped, Bravia API service is reloading")
|
||||
_LOGGER.debug(
|
||||
"Update for %s skipped: the Bravia API service is reloading",
|
||||
self.config_entry.title,
|
||||
)
|
||||
return
|
||||
raise UpdateFailed("Error communicating with device") from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error_not_found",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff):
|
||||
self.is_on = False
|
||||
self.connected = False
|
||||
_LOGGER.debug("Update skipped, Bravia TV is off")
|
||||
_LOGGER.debug(
|
||||
"Update for %s skipped: the TV is turned off", self.config_entry.title
|
||||
)
|
||||
except BraviaError as err:
|
||||
self.is_on = False
|
||||
self.connected = False
|
||||
raise UpdateFailed("Error communicating with device") from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"device": self.config_entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_update_volume(self) -> None:
|
||||
"""Update volume information."""
|
||||
|
||||
@@ -55,5 +55,22 @@
|
||||
"name": "Terminate apps"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"command_error": {
|
||||
"message": "Error sending command to {device}: {error}"
|
||||
},
|
||||
"command_error_not_found": {
|
||||
"message": "Error sending command to {device}: the Bravia API service is reloading"
|
||||
},
|
||||
"command_error_offline": {
|
||||
"message": "Error sending command to {device}: the TV is turned off"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "Error updating data for {device}: {error}"
|
||||
},
|
||||
"update_error_not_found": {
|
||||
"message": "Error updating data for {device}: the Bravia API service is stuck"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.17.0"]
|
||||
"requirements": ["bthome-ble==3.16.0"]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [HVACMode]
|
||||
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,14 +31,11 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -169,6 +169,7 @@ FRIENDS_OF_HUE_SWITCH = {
|
||||
}
|
||||
|
||||
RODRET_REMOTE_MODEL = "RODRET Dimmer"
|
||||
RODRET_REMOTE_MODEL_2 = "RODRET wireless dimmer"
|
||||
RODRET_REMOTE = {
|
||||
(CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
|
||||
(CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
|
||||
@@ -624,6 +625,7 @@ REMOTES = {
|
||||
HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE,
|
||||
FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH,
|
||||
RODRET_REMOTE_MODEL: RODRET_REMOTE,
|
||||
RODRET_REMOTE_MODEL_2: RODRET_REMOTE,
|
||||
SOMRIG_REMOTE_MODEL: SOMRIG_REMOTE,
|
||||
STYRBAR_REMOTE_MODEL: STYRBAR_REMOTE,
|
||||
SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER,
|
||||
|
||||
@@ -28,10 +28,11 @@ async def async_setup_entry(
|
||||
DemoHumidifier(
|
||||
name="Humidifier",
|
||||
mode=None,
|
||||
target_humidity=68,
|
||||
target_humidity=65,
|
||||
current_humidity=45,
|
||||
action=HumidifierAction.HUMIDIFYING,
|
||||
device_class=HumidifierDeviceClass.HUMIDIFIER,
|
||||
target_humidity_step=5,
|
||||
),
|
||||
DemoHumidifier(
|
||||
name="Dehumidifier",
|
||||
@@ -66,6 +67,7 @@ class DemoHumidifier(HumidifierEntity):
|
||||
is_on: bool = True,
|
||||
action: HumidifierAction | None = None,
|
||||
device_class: HumidifierDeviceClass | None = None,
|
||||
target_humidity_step: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the humidifier device."""
|
||||
self._attr_name = name
|
||||
@@ -79,6 +81,7 @@ class DemoHumidifier(HumidifierEntity):
|
||||
self._attr_mode = mode
|
||||
self._attr_available_modes = available_modes
|
||||
self._attr_device_class = device_class
|
||||
self._attr_target_humidity_step = target_humidity_step
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==3.6.1"]
|
||||
"requirements": ["aiodns==4.0.0"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["easyenergy==2.1.2"],
|
||||
"requirements": ["easyenergy==2.2.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.1"]
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
|
||||
name=device.name,
|
||||
connections={(CONNECTION_NETWORK_MAC, device.mac_address)},
|
||||
manufacturer="EHEIM",
|
||||
model=device.device_type.model_name,
|
||||
model=device.model_name,
|
||||
identifiers={(DOMAIN, device.mac_address)},
|
||||
suggested_area=device.aquarium_name,
|
||||
sw_version=device.sw_version,
|
||||
@@ -59,9 +59,9 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
|
||||
def exception_handler[_EntityT: EheimDigitalEntity[EheimDigitalDevice], **_P](
|
||||
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate AirGradient calls to handle exceptions.
|
||||
"""Decorate eheimdigital calls to handle exceptions.
|
||||
|
||||
A decorator that wraps the passed in function, catches AirGradient errors.
|
||||
A decorator that wraps the passed in function, catches eheimdigital errors.
|
||||
"""
|
||||
|
||||
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.filter import EheimDigitalFilter
|
||||
from eheimdigital.heater import EheimDigitalHeater
|
||||
from eheimdigital.types import HeaterUnit
|
||||
|
||||
@@ -21,6 +22,7 @@ from homeassistant.const import (
|
||||
PRECISION_WHOLE,
|
||||
EntityCategory,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -42,6 +44,34 @@ class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice](
|
||||
uom_fn: Callable[[_DeviceT], str] | None = None
|
||||
|
||||
|
||||
FILTER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalFilter], ...] = (
|
||||
EheimDigitalNumberDescription[EheimDigitalFilter](
|
||||
key="high_pulse_time",
|
||||
translation_key="high_pulse_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=PRECISION_WHOLE,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_min_value=5,
|
||||
native_max_value=200000,
|
||||
value_fn=lambda device: device.high_pulse_time,
|
||||
set_value_fn=lambda device, value: device.set_high_pulse_time(int(value)),
|
||||
),
|
||||
EheimDigitalNumberDescription[EheimDigitalFilter](
|
||||
key="low_pulse_time",
|
||||
translation_key="low_pulse_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=PRECISION_WHOLE,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_min_value=5,
|
||||
native_max_value=200000,
|
||||
value_fn=lambda device: device.low_pulse_time,
|
||||
set_value_fn=lambda device, value: device.set_low_pulse_time(int(value)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalNumberDescription[EheimDigitalClassicVario], ...
|
||||
] = (
|
||||
@@ -145,6 +175,13 @@ async def async_setup_entry(
|
||||
)
|
||||
for description in CLASSICVARIO_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalFilter):
|
||||
entities.extend(
|
||||
EheimDigitalNumber[EheimDigitalFilter](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in FILTER_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalHeater):
|
||||
entities.extend(
|
||||
EheimDigitalNumber[EheimDigitalHeater](
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, override
|
||||
from typing import Any, Literal, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.types import FilterMode
|
||||
from eheimdigital.filter import EheimDigitalFilter
|
||||
from eheimdigital.types import (
|
||||
FilterMode,
|
||||
FilterModeProf,
|
||||
UnitOfMeasurement as EheimDigitalUnitOfMeasurement,
|
||||
)
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory, UnitOfFrequency, UnitOfVolumeFlowRate
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -24,8 +30,109 @@ class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice](
|
||||
):
|
||||
"""Class describing EHEIM Digital select entities."""
|
||||
|
||||
options_fn: Callable[[_DeviceT], list[str]] | None = None
|
||||
use_api_unit: Literal[True] | None = None
|
||||
value_fn: Callable[[_DeviceT], str | None]
|
||||
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
|
||||
set_value_fn: Callable[[_DeviceT, str], Awaitable[None] | None]
|
||||
|
||||
|
||||
FILTER_DESCRIPTIONS: tuple[EheimDigitalSelectDescription[EheimDigitalFilter], ...] = (
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="filter_mode",
|
||||
translation_key="filter_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=[item.lower() for item in FilterModeProf._member_names_],
|
||||
value_fn=lambda device: device.filter_mode.name.lower(),
|
||||
set_value_fn=lambda device, value: device.set_filter_mode(
|
||||
FilterModeProf[value.upper()]
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="manual_speed",
|
||||
translation_key="manual_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_manual_values],
|
||||
value_fn=lambda device: str(device.manual_speed),
|
||||
set_value_fn=lambda device, value: device.set_manual_speed(float(value)),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="const_flow_speed",
|
||||
translation_key="const_flow_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(device.filter_const_flow_values[device.const_flow]),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_const_flow(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="day_speed",
|
||||
translation_key="day_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(device.filter_const_flow_values[device.day_speed]),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_day_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="night_speed",
|
||||
translation_key="night_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(
|
||||
device.filter_const_flow_values[device.night_speed]
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_night_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="high_pulse_speed",
|
||||
translation_key="high_pulse_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(
|
||||
device.filter_const_flow_values[device.high_pulse_speed]
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_high_pulse_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
EheimDigitalSelectDescription[EheimDigitalFilter](
|
||||
key="low_pulse_speed",
|
||||
translation_key="low_pulse_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
use_api_unit=True,
|
||||
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
|
||||
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
|
||||
value_fn=lambda device: str(
|
||||
device.filter_const_flow_values[device.low_pulse_speed]
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_low_pulse_speed(
|
||||
device.filter_const_flow_values.index(int(value))
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -34,11 +141,7 @@ CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalSelectDescription[EheimDigitalClassicVario](
|
||||
key="filter_mode",
|
||||
translation_key="filter_mode",
|
||||
value_fn=(
|
||||
lambda device: device.filter_mode.name.lower()
|
||||
if device.filter_mode is not None
|
||||
else None
|
||||
),
|
||||
value_fn=lambda device: device.filter_mode.name.lower(),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
|
||||
),
|
||||
@@ -68,6 +171,11 @@ async def async_setup_entry(
|
||||
)
|
||||
for description in CLASSICVARIO_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalFilter):
|
||||
entities.extend(
|
||||
EheimDigitalFilterSelect(coordinator, device, description)
|
||||
for description in FILTER_DESCRIPTIONS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -82,6 +190,8 @@ class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
|
||||
|
||||
entity_description: EheimDigitalSelectDescription[_DeviceT]
|
||||
|
||||
_attr_options: list[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
@@ -91,13 +201,49 @@ class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
|
||||
"""Initialize an EHEIM Digital select entity."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
if description.options_fn is not None:
|
||||
self._attr_options = description.options_fn(device)
|
||||
elif description.options is not None:
|
||||
self._attr_options = description.options
|
||||
self._attr_unique_id = f"{self._device_address}_{description.key}"
|
||||
|
||||
@override
|
||||
@exception_handler
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
return await self.entity_description.set_value_fn(self._device, option)
|
||||
if await_return := self.entity_description.set_value_fn(self._device, option):
|
||||
return await await_return
|
||||
return None
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
self._attr_current_option = self.entity_description.value_fn(self._device)
|
||||
|
||||
|
||||
class EheimDigitalFilterSelect(EheimDigitalSelect[EheimDigitalFilter]):
|
||||
"""Represent an EHEIM Digital Filter select entity."""
|
||||
|
||||
entity_description: EheimDigitalSelectDescription[EheimDigitalFilter]
|
||||
_attr_native_unit_of_measurement: str | None
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
if (
|
||||
self.entity_description.options is None
|
||||
and self.entity_description.options_fn is not None
|
||||
):
|
||||
self._attr_options = self.entity_description.options_fn(self._device)
|
||||
if self.entity_description.use_api_unit:
|
||||
if (
|
||||
self.entity_description.unit_of_measurement
|
||||
== UnitOfVolumeFlowRate.LITERS_PER_HOUR
|
||||
and self._device.usrdta["unit"]
|
||||
== int(EheimDigitalUnitOfMeasurement.US_CUSTOMARY)
|
||||
):
|
||||
self._attr_native_unit_of_measurement = (
|
||||
UnitOfVolumeFlowRate.GALLONS_PER_HOUR
|
||||
)
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = (
|
||||
self.entity_description.unit_of_measurement
|
||||
)
|
||||
super()._async_update_attrs()
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.filter import EheimDigitalFilter
|
||||
from eheimdigital.types import FilterErrorCode
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -13,7 +14,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfFrequency, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -33,6 +34,27 @@ class EheimDigitalSensorDescription[_DeviceT: EheimDigitalDevice](
|
||||
value_fn: Callable[[_DeviceT], float | str | None]
|
||||
|
||||
|
||||
FILTER_DESCRIPTIONS: tuple[EheimDigitalSensorDescription[EheimDigitalFilter], ...] = (
|
||||
EheimDigitalSensorDescription[EheimDigitalFilter](
|
||||
key="current_speed",
|
||||
translation_key="current_speed",
|
||||
value_fn=lambda device: device.current_speed,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
suggested_display_precision=1,
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
),
|
||||
EheimDigitalSensorDescription[EheimDigitalFilter](
|
||||
key="service_hours",
|
||||
translation_key="service_hours",
|
||||
value_fn=lambda device: device.service_hours,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||
suggested_unit_of_measurement=UnitOfTime.DAYS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalSensorDescription[EheimDigitalClassicVario], ...
|
||||
] = (
|
||||
@@ -54,11 +76,7 @@ CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalSensorDescription[EheimDigitalClassicVario](
|
||||
key="error_code",
|
||||
translation_key="error_code",
|
||||
value_fn=(
|
||||
lambda device: device.error_code.name.lower()
|
||||
if device.error_code is not None
|
||||
else None
|
||||
),
|
||||
value_fn=lambda device: device.error_code.name.lower(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[name.lower() for name in FilterErrorCode._member_names_],
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -80,6 +98,13 @@ async def async_setup_entry(
|
||||
"""Set up the light entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalSensor[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalFilter):
|
||||
entities += [
|
||||
EheimDigitalSensor[EheimDigitalFilter](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in FILTER_DESCRIPTIONS
|
||||
]
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities += [
|
||||
EheimDigitalSensor[EheimDigitalClassicVario](
|
||||
|
||||
@@ -61,6 +61,12 @@
|
||||
"day_speed": {
|
||||
"name": "Day speed"
|
||||
},
|
||||
"high_pulse_time": {
|
||||
"name": "High pulse duration"
|
||||
},
|
||||
"low_pulse_time": {
|
||||
"name": "Low pulse duration"
|
||||
},
|
||||
"manual_speed": {
|
||||
"name": "Manual speed"
|
||||
},
|
||||
@@ -78,13 +84,32 @@
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"const_flow_speed": {
|
||||
"name": "Constant flow speed"
|
||||
},
|
||||
"day_speed": {
|
||||
"name": "Day speed"
|
||||
},
|
||||
"filter_mode": {
|
||||
"name": "Filter mode",
|
||||
"state": {
|
||||
"bio": "Bio",
|
||||
"constant_flow": "Constant flow",
|
||||
"manual": "Manual",
|
||||
"pulse": "Pulse"
|
||||
}
|
||||
},
|
||||
"high_pulse_speed": {
|
||||
"name": "High pulse speed"
|
||||
},
|
||||
"low_pulse_speed": {
|
||||
"name": "Low pulse speed"
|
||||
},
|
||||
"manual_speed": {
|
||||
"name": "Manual speed"
|
||||
},
|
||||
"night_speed": {
|
||||
"name": "Night speed"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -99,8 +124,17 @@
|
||||
"rotor_stuck": "Rotor stuck"
|
||||
}
|
||||
},
|
||||
"operating_time": {
|
||||
"name": "Operating time"
|
||||
},
|
||||
"service_hours": {
|
||||
"name": "Remaining hours until service"
|
||||
},
|
||||
"turn_feeding_time": {
|
||||
"name": "Remaining off time after feeding"
|
||||
},
|
||||
"turn_off_time": {
|
||||
"name": "Remaining off time"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.filter import EheimDigitalFilter
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -30,8 +31,8 @@ async def async_setup_entry(
|
||||
"""Set up the switch entities for one or multiple devices."""
|
||||
entities: list[SwitchEntity] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.append(EheimDigitalClassicVarioSwitch(coordinator, device)) # noqa: PERF401
|
||||
if isinstance(device, (EheimDigitalClassicVario, EheimDigitalFilter)):
|
||||
entities.append(EheimDigitalFilterSwitch(coordinator, device)) # noqa: PERF401
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -39,10 +40,10 @@ async def async_setup_entry(
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalClassicVarioSwitch(
|
||||
EheimDigitalEntity[EheimDigitalClassicVario], SwitchEntity
|
||||
class EheimDigitalFilterSwitch(
|
||||
EheimDigitalEntity[EheimDigitalClassicVario | EheimDigitalFilter], SwitchEntity
|
||||
):
|
||||
"""Represent an EHEIM Digital classicVARIO switch entity."""
|
||||
"""Represent an EHEIM Digital classicVARIO or filter switch entity."""
|
||||
|
||||
_attr_translation_key = "filter_active"
|
||||
_attr_name = None
|
||||
@@ -50,9 +51,9 @@ class EheimDigitalClassicVarioSwitch(
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: EheimDigitalClassicVario,
|
||||
device: EheimDigitalClassicVario | EheimDigitalFilter,
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital classicVARIO switch entity."""
|
||||
"""Initialize an EHEIM Digital classicVARIO or filter switch entity."""
|
||||
super().__init__(coordinator, device)
|
||||
self._attr_unique_id = device.mac_address
|
||||
self._async_update_attrs()
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, final, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.filter import EheimDigitalFilter
|
||||
from eheimdigital.heater import EheimDigitalHeater
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
@@ -28,6 +29,23 @@ class EheimDigitalTimeDescription[_DeviceT: EheimDigitalDevice](TimeEntityDescri
|
||||
set_value_fn: Callable[[_DeviceT, time], Awaitable[None]]
|
||||
|
||||
|
||||
FILTER_DESCRIPTIONS: tuple[EheimDigitalTimeDescription[EheimDigitalFilter], ...] = (
|
||||
EheimDigitalTimeDescription[EheimDigitalFilter](
|
||||
key="day_start_time",
|
||||
translation_key="day_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: device.day_start_time,
|
||||
set_value_fn=lambda device, value: device.set_day_start_time(value),
|
||||
),
|
||||
EheimDigitalTimeDescription[EheimDigitalFilter](
|
||||
key="night_start_time",
|
||||
translation_key="night_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: device.night_start_time,
|
||||
set_value_fn=lambda device, value: device.set_night_start_time(value),
|
||||
),
|
||||
)
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalTimeDescription[EheimDigitalClassicVario], ...
|
||||
] = (
|
||||
@@ -79,6 +97,13 @@ async def async_setup_entry(
|
||||
"""Set up the time entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalTime[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalFilter):
|
||||
entities.extend(
|
||||
EheimDigitalTime[EheimDigitalFilter](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in FILTER_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
EheimDigitalTime[EheimDigitalClassicVario](
|
||||
|
||||
@@ -59,38 +59,13 @@ class FlowToGridSourceType(TypedDict):
|
||||
number_energy_price: float | None # Price for energy ($/kWh)
|
||||
|
||||
|
||||
class PowerConfig(TypedDict, total=False):
|
||||
"""Dictionary holding power sensor configuration options.
|
||||
|
||||
Users can configure power sensors in three ways:
|
||||
1. Standard: single sensor (positive=discharge/from_grid, negative=charge/to_grid)
|
||||
2. Inverted: single sensor with opposite polarity (needs to be multiplied by -1)
|
||||
3. Two sensors: separate positive sensors for each direction
|
||||
"""
|
||||
|
||||
# Standard: single sensor (positive=discharge/from_grid, negative=charge/to_grid)
|
||||
stat_rate: str
|
||||
|
||||
# Inverted: single sensor with opposite polarity (needs to be multiplied by -1)
|
||||
stat_rate_inverted: str
|
||||
|
||||
# Two sensors: separate positive sensors for each direction
|
||||
# Result = stat_rate_from - stat_rate_to (positive when net outflow)
|
||||
stat_rate_from: str # Battery: discharge, Grid: consumption
|
||||
stat_rate_to: str # Battery: charge, Grid: return
|
||||
|
||||
|
||||
class GridPowerSourceType(TypedDict, total=False):
|
||||
class GridPowerSourceType(TypedDict):
|
||||
"""Dictionary holding the source of grid power consumption."""
|
||||
|
||||
# statistic_id of a power meter (kW)
|
||||
# negative values indicate grid return
|
||||
# This is either the original sensor or a generated template sensor
|
||||
stat_rate: str
|
||||
|
||||
# User's original power sensor configuration
|
||||
power_config: PowerConfig
|
||||
|
||||
|
||||
class GridSourceType(TypedDict):
|
||||
"""Dictionary holding the source of grid energy consumption."""
|
||||
@@ -122,12 +97,8 @@ class BatterySourceType(TypedDict):
|
||||
stat_energy_from: str
|
||||
stat_energy_to: str
|
||||
# positive when discharging, negative when charging
|
||||
# This is either the original sensor or a generated template sensor
|
||||
stat_rate: NotRequired[str]
|
||||
|
||||
# User's original power sensor configuration
|
||||
power_config: NotRequired[PowerConfig]
|
||||
|
||||
|
||||
class GasSourceType(TypedDict):
|
||||
"""Dictionary holding the source of gas consumption."""
|
||||
@@ -240,47 +211,10 @@ FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_power_config(val: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate power_config has at least one option."""
|
||||
if not val:
|
||||
raise vol.Invalid("power_config must have at least one option")
|
||||
return val
|
||||
|
||||
|
||||
POWER_CONFIG_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Exclusive("stat_rate", "power_source"): str,
|
||||
vol.Exclusive("stat_rate_inverted", "power_source"): str,
|
||||
# stat_rate_from/stat_rate_to: two sensors for bidirectional power
|
||||
# Battery: from=discharge (out), to=charge (in)
|
||||
# Grid: from=consumption, to=return
|
||||
vol.Inclusive("stat_rate_from", "two_sensors"): str,
|
||||
vol.Inclusive("stat_rate_to", "two_sensors"): str,
|
||||
}
|
||||
),
|
||||
_validate_power_config,
|
||||
)
|
||||
|
||||
|
||||
def _validate_grid_power_source(val: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate grid power source has either stat_rate or power_config."""
|
||||
if "stat_rate" not in val and "power_config" not in val:
|
||||
raise vol.Invalid("Either stat_rate or power_config is required")
|
||||
return val
|
||||
|
||||
|
||||
GRID_POWER_SOURCE_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
# stat_rate and power_config are both optional schema keys, but the validator
|
||||
# requires that at least one is provided; power_config takes precedence
|
||||
vol.Optional("stat_rate"): str,
|
||||
vol.Optional("power_config"): POWER_CONFIG_SCHEMA,
|
||||
}
|
||||
),
|
||||
_validate_grid_power_source,
|
||||
GRID_POWER_SOURCE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("stat_rate"): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -291,7 +225,7 @@ def _generate_unique_value_validator(key: str) -> Callable[[list[dict]], list[di
|
||||
val: list[dict],
|
||||
) -> list[dict]:
|
||||
"""Ensure that the user doesn't add duplicate values."""
|
||||
counts = Counter(item.get(key) for item in val if item.get(key) is not None)
|
||||
counts = Counter(flow_from[key] for flow_from in val)
|
||||
|
||||
for value, count in counts.items():
|
||||
if count > 1:
|
||||
@@ -333,10 +267,7 @@ BATTERY_SOURCE_SCHEMA = vol.Schema(
|
||||
vol.Required("type"): "battery",
|
||||
vol.Required("stat_energy_from"): str,
|
||||
vol.Required("stat_energy_to"): str,
|
||||
# Both stat_rate and power_config are optional
|
||||
# If power_config is provided, it takes precedence and stat_rate is overwritten
|
||||
vol.Optional("stat_rate"): str,
|
||||
vol.Optional("power_config"): POWER_CONFIG_SCHEMA,
|
||||
}
|
||||
)
|
||||
GAS_SOURCE_SCHEMA = vol.Schema(
|
||||
@@ -456,12 +387,6 @@ class EnergyManager:
|
||||
if key in update:
|
||||
data[key] = update[key]
|
||||
|
||||
# Process energy sources and set stat_rate for power configs
|
||||
if "energy_sources" in update:
|
||||
data["energy_sources"] = self._process_energy_sources(
|
||||
data["energy_sources"]
|
||||
)
|
||||
|
||||
self.data = data
|
||||
self._store.async_delay_save(lambda: data, 60)
|
||||
|
||||
@@ -470,74 +395,6 @@ class EnergyManager:
|
||||
|
||||
await asyncio.gather(*(listener() for listener in self._update_listeners))
|
||||
|
||||
def _process_energy_sources(self, sources: list[SourceType]) -> list[SourceType]:
|
||||
"""Process energy sources and set stat_rate for power configs."""
|
||||
from .helpers import generate_power_sensor_entity_id # noqa: PLC0415
|
||||
|
||||
processed: list[SourceType] = []
|
||||
for source in sources:
|
||||
if source["type"] == "battery":
|
||||
source = self._process_battery_power(
|
||||
source, generate_power_sensor_entity_id
|
||||
)
|
||||
elif source["type"] == "grid":
|
||||
source = self._process_grid_power(
|
||||
source, generate_power_sensor_entity_id
|
||||
)
|
||||
processed.append(source)
|
||||
return processed
|
||||
|
||||
def _process_battery_power(
|
||||
self,
|
||||
source: BatterySourceType,
|
||||
generate_entity_id: Callable[[str, PowerConfig], str],
|
||||
) -> BatterySourceType:
|
||||
"""Set stat_rate for battery if power_config is specified."""
|
||||
if "power_config" not in source:
|
||||
return source
|
||||
|
||||
config = source["power_config"]
|
||||
|
||||
# If power_config has stat_rate (standard), just use it directly
|
||||
if "stat_rate" in config:
|
||||
return {**source, "stat_rate": config["stat_rate"]}
|
||||
|
||||
# For inverted or two-sensor config, set stat_rate to the generated entity_id
|
||||
entity_id = generate_entity_id("battery", config)
|
||||
if entity_id:
|
||||
return {**source, "stat_rate": entity_id}
|
||||
|
||||
return source
|
||||
|
||||
def _process_grid_power(
|
||||
self,
|
||||
source: GridSourceType,
|
||||
generate_entity_id: Callable[[str, PowerConfig], str],
|
||||
) -> GridSourceType:
|
||||
"""Set stat_rate for grid power sources if power_config is specified."""
|
||||
if "power" not in source:
|
||||
return source
|
||||
|
||||
processed_power: list[GridPowerSourceType] = []
|
||||
for power in source["power"]:
|
||||
if "power_config" in power:
|
||||
config = power["power_config"]
|
||||
|
||||
# If power_config has stat_rate (standard), just use it directly
|
||||
if "stat_rate" in config:
|
||||
processed_power.append({**power, "stat_rate": config["stat_rate"]})
|
||||
continue
|
||||
|
||||
# For inverted or two-sensor config, set stat_rate to generated entity_id
|
||||
entity_id = generate_entity_id("grid", config)
|
||||
if entity_id:
|
||||
processed_power.append({**power, "stat_rate": entity_id})
|
||||
continue
|
||||
|
||||
processed_power.append(power)
|
||||
|
||||
return {**source, "power": processed_power}
|
||||
|
||||
@callback
|
||||
def async_listen_updates(self, update_listener: Callable[[], Awaitable]) -> None:
|
||||
"""Listen for data updates."""
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"""Helpers for the Energy integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .data import PowerConfig
|
||||
|
||||
|
||||
def generate_power_sensor_unique_id(source_type: str, config: PowerConfig) -> str:
|
||||
"""Generate a unique ID for a power transform sensor."""
|
||||
if "stat_rate_inverted" in config:
|
||||
sensor_id = config["stat_rate_inverted"].replace(".", "_")
|
||||
return f"energy_power_{source_type}_inv_{sensor_id}"
|
||||
if "stat_rate_from" in config and "stat_rate_to" in config:
|
||||
from_id = config["stat_rate_from"].replace(".", "_")
|
||||
to_id = config["stat_rate_to"].replace(".", "_")
|
||||
return f"energy_power_{source_type}_combined_{from_id}_{to_id}"
|
||||
return ""
|
||||
|
||||
|
||||
def generate_power_sensor_entity_id(source_type: str, config: PowerConfig) -> str:
|
||||
"""Generate an entity ID for a power transform sensor."""
|
||||
if "stat_rate_inverted" in config:
|
||||
# Use source sensor name with _inverted suffix
|
||||
source = config["stat_rate_inverted"]
|
||||
if source.startswith("sensor."):
|
||||
return f"{source}_inverted"
|
||||
return f"sensor.{source.replace('.', '_')}_inverted"
|
||||
if "stat_rate_from" in config and "stat_rate_to" in config:
|
||||
# Include sensor IDs to avoid collisions when multiple combined configs exist
|
||||
from_sensor = config["stat_rate_from"].removeprefix("sensor.")
|
||||
return f"sensor.energy_{source_type}_{from_sensor}_power"
|
||||
return ""
|
||||
@@ -19,12 +19,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.components.sensor.recorder import ( # pylint: disable=hass-component-root-import
|
||||
reset_detected,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
State,
|
||||
@@ -41,8 +36,7 @@ from homeassistant.util import dt as dt_util, unit_conversion
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import EnergyManager, PowerConfig, async_get_manager
|
||||
from .helpers import generate_power_sensor_entity_id, generate_power_sensor_unique_id
|
||||
from .data import EnergyManager, async_get_manager
|
||||
|
||||
SUPPORTED_STATE_CLASSES = {
|
||||
SensorStateClass.MEASUREMENT,
|
||||
@@ -143,7 +137,6 @@ class SensorManager:
|
||||
self.manager = manager
|
||||
self.async_add_entities = async_add_entities
|
||||
self.current_entities: dict[tuple[str, str | None, str], EnergyCostSensor] = {}
|
||||
self.current_power_entities: dict[str, EnergyPowerSensor] = {}
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start."""
|
||||
@@ -154,9 +147,8 @@ class SensorManager:
|
||||
|
||||
async def _process_manager_data(self) -> None:
|
||||
"""Process manager data."""
|
||||
to_add: list[EnergyCostSensor | EnergyPowerSensor] = []
|
||||
to_add: list[EnergyCostSensor] = []
|
||||
to_remove = dict(self.current_entities)
|
||||
power_to_remove = dict(self.current_power_entities)
|
||||
|
||||
async def finish() -> None:
|
||||
if to_add:
|
||||
@@ -167,10 +159,6 @@ class SensorManager:
|
||||
self.current_entities.pop(key)
|
||||
await entity.async_remove()
|
||||
|
||||
for power_key, power_entity in power_to_remove.items():
|
||||
self.current_power_entities.pop(power_key)
|
||||
await power_entity.async_remove()
|
||||
|
||||
if not self.manager.data:
|
||||
await finish()
|
||||
return
|
||||
@@ -197,13 +185,6 @@ class SensorManager:
|
||||
to_remove,
|
||||
)
|
||||
|
||||
# Process power sensors for battery and grid sources
|
||||
self._process_power_sensor_data(
|
||||
energy_source,
|
||||
to_add,
|
||||
power_to_remove,
|
||||
)
|
||||
|
||||
await finish()
|
||||
|
||||
@callback
|
||||
@@ -211,7 +192,7 @@ class SensorManager:
|
||||
self,
|
||||
adapter: SourceAdapter,
|
||||
config: Mapping[str, Any],
|
||||
to_add: list[EnergyCostSensor | EnergyPowerSensor],
|
||||
to_add: list[EnergyCostSensor],
|
||||
to_remove: dict[tuple[str, str | None, str], EnergyCostSensor],
|
||||
) -> None:
|
||||
"""Process sensor data."""
|
||||
@@ -239,74 +220,6 @@ class SensorManager:
|
||||
)
|
||||
to_add.append(self.current_entities[key])
|
||||
|
||||
@callback
|
||||
def _process_power_sensor_data(
|
||||
self,
|
||||
energy_source: Mapping[str, Any],
|
||||
to_add: list[EnergyCostSensor | EnergyPowerSensor],
|
||||
to_remove: dict[str, EnergyPowerSensor],
|
||||
) -> None:
|
||||
"""Process power sensor data for battery and grid sources."""
|
||||
source_type = energy_source.get("type")
|
||||
|
||||
if source_type == "battery":
|
||||
power_config = energy_source.get("power_config")
|
||||
if power_config and self._needs_power_sensor(power_config):
|
||||
self._create_or_keep_power_sensor(
|
||||
source_type, power_config, to_add, to_remove
|
||||
)
|
||||
|
||||
elif source_type == "grid":
|
||||
for power in energy_source.get("power", []):
|
||||
power_config = power.get("power_config")
|
||||
if power_config and self._needs_power_sensor(power_config):
|
||||
self._create_or_keep_power_sensor(
|
||||
source_type, power_config, to_add, to_remove
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _needs_power_sensor(power_config: PowerConfig) -> bool:
|
||||
"""Check if power_config needs a transform sensor."""
|
||||
# Only create sensors for inverted or two-sensor configs
|
||||
# Standard stat_rate configs don't need a transform sensor
|
||||
return "stat_rate_inverted" in power_config or (
|
||||
"stat_rate_from" in power_config and "stat_rate_to" in power_config
|
||||
)
|
||||
|
||||
def _create_or_keep_power_sensor(
|
||||
self,
|
||||
source_type: str,
|
||||
power_config: PowerConfig,
|
||||
to_add: list[EnergyCostSensor | EnergyPowerSensor],
|
||||
to_remove: dict[str, EnergyPowerSensor],
|
||||
) -> None:
|
||||
"""Create a power sensor or keep an existing one."""
|
||||
unique_id = generate_power_sensor_unique_id(source_type, power_config)
|
||||
if not unique_id:
|
||||
return
|
||||
|
||||
# If entity already exists, keep it
|
||||
if unique_id in to_remove:
|
||||
to_remove.pop(unique_id)
|
||||
return
|
||||
|
||||
# If we already have this entity, skip
|
||||
if unique_id in self.current_power_entities:
|
||||
return
|
||||
|
||||
entity_id = generate_power_sensor_entity_id(source_type, power_config)
|
||||
if not entity_id:
|
||||
return
|
||||
|
||||
sensor = EnergyPowerSensor(
|
||||
source_type,
|
||||
power_config,
|
||||
unique_id,
|
||||
entity_id,
|
||||
)
|
||||
self.current_power_entities[unique_id] = sensor
|
||||
to_add.append(sensor)
|
||||
|
||||
|
||||
def _set_result_unless_done(future: asyncio.Future[None]) -> None:
|
||||
"""Set the result of a future unless it is done."""
|
||||
@@ -582,196 +495,3 @@ class EnergyCostSensor(SensorEntity):
|
||||
prefix = self._config[self._adapter.stat_energy_key]
|
||||
|
||||
return f"{prefix}_{self._adapter.source_type}_{self._adapter.entity_id_suffix}"
|
||||
|
||||
|
||||
class EnergyPowerSensor(SensorEntity):
|
||||
"""Transform power sensor values (invert or combine two sensors).
|
||||
|
||||
This sensor handles non-standard power sensor configurations for the energy
|
||||
dashboard by either inverting polarity or combining two positive sensors.
|
||||
"""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_device_class = SensorDeviceClass.POWER
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_type: str,
|
||||
config: PowerConfig,
|
||||
unique_id: str,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__()
|
||||
self._source_type = source_type
|
||||
self._config: PowerConfig = config
|
||||
self._attr_unique_id = unique_id
|
||||
self.entity_id = entity_id
|
||||
self._source_sensors: list[str] = []
|
||||
self._is_inverted = "stat_rate_inverted" in config
|
||||
self._is_combined = "stat_rate_from" in config and "stat_rate_to" in config
|
||||
|
||||
# Determine source sensors
|
||||
if self._is_inverted:
|
||||
self._source_sensors = [config["stat_rate_inverted"]]
|
||||
elif self._is_combined:
|
||||
self._source_sensors = [
|
||||
config["stat_rate_from"],
|
||||
config["stat_rate_to"],
|
||||
]
|
||||
|
||||
# add_finished is set when either async_added_to_hass or add_to_platform_abort
|
||||
# is called
|
||||
self.add_finished: asyncio.Future[None] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
if self._is_inverted:
|
||||
source = self.hass.states.get(self._source_sensors[0])
|
||||
return source is not None and source.state not in (
|
||||
"unknown",
|
||||
"unavailable",
|
||||
)
|
||||
if self._is_combined:
|
||||
discharge = self.hass.states.get(self._source_sensors[0])
|
||||
charge = self.hass.states.get(self._source_sensors[1])
|
||||
return (
|
||||
discharge is not None
|
||||
and charge is not None
|
||||
and discharge.state not in ("unknown", "unavailable")
|
||||
and charge.state not in ("unknown", "unavailable")
|
||||
)
|
||||
return True
|
||||
|
||||
@callback
|
||||
def _update_state(self) -> None:
|
||||
"""Update the sensor state based on source sensors."""
|
||||
if self._is_inverted:
|
||||
source_state = self.hass.states.get(self._source_sensors[0])
|
||||
if source_state is None or source_state.state in ("unknown", "unavailable"):
|
||||
self._attr_native_value = None
|
||||
return
|
||||
try:
|
||||
value = float(source_state.state)
|
||||
self._attr_native_value = value * -1
|
||||
except ValueError:
|
||||
self._attr_native_value = None
|
||||
|
||||
elif self._is_combined:
|
||||
discharge_state = self.hass.states.get(self._source_sensors[0])
|
||||
charge_state = self.hass.states.get(self._source_sensors[1])
|
||||
|
||||
if (
|
||||
discharge_state is None
|
||||
or charge_state is None
|
||||
or discharge_state.state in ("unknown", "unavailable")
|
||||
or charge_state.state in ("unknown", "unavailable")
|
||||
):
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
try:
|
||||
discharge = float(discharge_state.state)
|
||||
charge = float(charge_state.state)
|
||||
|
||||
# Get units from state attributes
|
||||
discharge_unit = discharge_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
charge_unit = charge_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
# Convert to Watts if units are present
|
||||
if discharge_unit:
|
||||
discharge = unit_conversion.PowerConverter.convert(
|
||||
discharge, discharge_unit, UnitOfPower.WATT
|
||||
)
|
||||
if charge_unit:
|
||||
charge = unit_conversion.PowerConverter.convert(
|
||||
charge, charge_unit, UnitOfPower.WATT
|
||||
)
|
||||
|
||||
self._attr_native_value = discharge - charge
|
||||
except ValueError:
|
||||
self._attr_native_value = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
# Set name based on source sensor(s)
|
||||
if self._source_sensors:
|
||||
entity_reg = er.async_get(self.hass)
|
||||
device_id = None
|
||||
source_name = None
|
||||
# Check first sensor
|
||||
if source_entry := entity_reg.async_get(self._source_sensors[0]):
|
||||
device_id = source_entry.device_id
|
||||
# For combined mode, always use Watts because we may have different source units; for inverted mode, copy source unit
|
||||
if self._is_combined:
|
||||
self._attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = (
|
||||
source_entry.unit_of_measurement
|
||||
)
|
||||
# Get source name from registry
|
||||
source_name = source_entry.name or source_entry.original_name
|
||||
# Assign power sensor to same device as source sensor(s)
|
||||
# Note: We use manual entity registry update instead of _attr_device_info
|
||||
# because device assignment depends on runtime information from the entity
|
||||
# registry (which source sensor has a device). This information isn't
|
||||
# available during __init__, and the entity is already registered before
|
||||
# async_added_to_hass runs, making the standard _attr_device_info pattern
|
||||
# incompatible with this use case.
|
||||
# If first sensor has no device and we have a second sensor, check it
|
||||
if not device_id and len(self._source_sensors) > 1:
|
||||
if source_entry := entity_reg.async_get(self._source_sensors[1]):
|
||||
device_id = source_entry.device_id
|
||||
# Update entity registry entry with device_id
|
||||
if device_id and (power_entry := entity_reg.async_get(self.entity_id)):
|
||||
entity_reg.async_update_entity(
|
||||
power_entry.entity_id, device_id=device_id
|
||||
)
|
||||
else:
|
||||
self._attr_has_entity_name = False
|
||||
|
||||
# Set name for inverted mode
|
||||
if self._is_inverted:
|
||||
if source_name:
|
||||
self._attr_name = f"{source_name} Inverted"
|
||||
else:
|
||||
# Fall back to entity_id if no name in registry
|
||||
sensor_name = split_entity_id(self._source_sensors[0])[1].replace(
|
||||
"_", " "
|
||||
)
|
||||
self._attr_name = f"{sensor_name.title()} Inverted"
|
||||
|
||||
# Set name for combined mode
|
||||
if self._is_combined:
|
||||
self._attr_name = f"{self._source_type.title()} Power"
|
||||
|
||||
self._update_state()
|
||||
|
||||
# Track state changes on all source sensors
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
self._source_sensors,
|
||||
self._async_state_changed_listener,
|
||||
)
|
||||
)
|
||||
_set_result_unless_done(self.add_finished)
|
||||
|
||||
@callback
|
||||
def _async_state_changed_listener(self, *_: Any) -> None:
|
||||
"""Handle source sensor state changes."""
|
||||
self._update_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def add_to_platform_abort(self) -> None:
|
||||
"""Abort adding an entity to a platform."""
|
||||
_set_result_unless_done(self.add_finished)
|
||||
super().add_to_platform_abort()
|
||||
|
||||
@@ -206,7 +206,7 @@ class EnvoyProductionSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Envoy production sensor entity."""
|
||||
|
||||
value_fn: Callable[[EnvoySystemProduction], int]
|
||||
on_phase: str | None
|
||||
on_phase: str | None = None
|
||||
|
||||
|
||||
PRODUCTION_SENSORS = (
|
||||
@@ -219,7 +219,6 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("watts_now"),
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyProductionSensorEntityDescription(
|
||||
key="daily_production",
|
||||
@@ -230,7 +229,6 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=2,
|
||||
value_fn=attrgetter("watt_hours_today"),
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyProductionSensorEntityDescription(
|
||||
key="seven_days_production",
|
||||
@@ -240,7 +238,6 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=1,
|
||||
value_fn=attrgetter("watt_hours_last_7_days"),
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyProductionSensorEntityDescription(
|
||||
key="lifetime_production",
|
||||
@@ -251,7 +248,6 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("watt_hours_lifetime"),
|
||||
on_phase=None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -277,7 +273,7 @@ class EnvoyConsumptionSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Envoy consumption sensor entity."""
|
||||
|
||||
value_fn: Callable[[EnvoySystemConsumption], int]
|
||||
on_phase: str | None
|
||||
on_phase: str | None = None
|
||||
|
||||
|
||||
CONSUMPTION_SENSORS = (
|
||||
@@ -290,7 +286,6 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("watts_now"),
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyConsumptionSensorEntityDescription(
|
||||
key="daily_consumption",
|
||||
@@ -301,7 +296,6 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=2,
|
||||
value_fn=attrgetter("watt_hours_today"),
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyConsumptionSensorEntityDescription(
|
||||
key="seven_days_consumption",
|
||||
@@ -311,7 +305,6 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=1,
|
||||
value_fn=attrgetter("watt_hours_last_7_days"),
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyConsumptionSensorEntityDescription(
|
||||
key="lifetime_consumption",
|
||||
@@ -322,7 +315,6 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("watt_hours_lifetime"),
|
||||
on_phase=None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -354,7 +346,6 @@ NET_CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("watts_now"),
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyConsumptionSensorEntityDescription(
|
||||
key="lifetime_balanced_net_consumption",
|
||||
@@ -366,7 +357,6 @@ NET_CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("watt_hours_lifetime"),
|
||||
on_phase=None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -395,7 +385,7 @@ class EnvoyCTSensorEntityDescription(SensorEntityDescription):
|
||||
[EnvoyMeterData],
|
||||
int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None,
|
||||
]
|
||||
on_phase: str | None
|
||||
on_phase: str | None = None
|
||||
cttype: str | None = None
|
||||
|
||||
|
||||
@@ -411,7 +401,6 @@ CT_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("energy_delivered"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key in (
|
||||
@@ -430,7 +419,6 @@ CT_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("energy_received"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key in (
|
||||
@@ -449,7 +437,6 @@ CT_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=3,
|
||||
value_fn=attrgetter("active_power"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key in (
|
||||
@@ -468,7 +455,6 @@ CT_SENSORS = (
|
||||
suggested_display_precision=1,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("frequency"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key, translation_key in (
|
||||
@@ -488,7 +474,6 @@ CT_SENSORS = (
|
||||
suggested_display_precision=1,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("voltage"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key, translation_key in (
|
||||
@@ -508,7 +493,6 @@ CT_SENSORS = (
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("current"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key in (
|
||||
@@ -526,7 +510,6 @@ CT_SENSORS = (
|
||||
suggested_display_precision=2,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("power_factor"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key in (
|
||||
@@ -544,7 +527,6 @@ CT_SENSORS = (
|
||||
options=list(CtMeterStatus),
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("metering_status"),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key, translation_key in (
|
||||
@@ -565,7 +547,6 @@ CT_SENSORS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
|
||||
on_phase=None,
|
||||
cttype=cttype,
|
||||
)
|
||||
for cttype, key, translation_key in (
|
||||
@@ -783,7 +764,7 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="available_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("available_energy"),
|
||||
),
|
||||
EnvoyEnchargeAggregateSensorEntityDescription(
|
||||
@@ -791,14 +772,14 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="reserve_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("backup_reserve"),
|
||||
),
|
||||
EnvoyEnchargeAggregateSensorEntityDescription(
|
||||
key="max_capacity",
|
||||
translation_key="max_capacity",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("max_available_capacity"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -77,9 +77,14 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
)
|
||||
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
|
||||
|
||||
self.has_triggers = await self.hass.async_add_executor_job(
|
||||
self.fritz.has_triggers
|
||||
)
|
||||
try:
|
||||
self.has_triggers = await self.hass.async_add_executor_job(
|
||||
self.fritz.has_triggers
|
||||
)
|
||||
except HTTPError:
|
||||
# Fritz!OS < 7.39 just don't have this api endpoint
|
||||
# so we need to fetch the HTTPError here and assume no triggers
|
||||
self.has_triggers = False
|
||||
LOGGER.debug("enable smarthome triggers: %s", self.has_triggers)
|
||||
|
||||
self.configuration_url = self.fritz.get_prefixed_host()
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251229.1"]
|
||||
"requirements": ["home-assistant-frontend==20260107.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_air_quality_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["google_air_quality_api==2.0.2"]
|
||||
"requirements": ["google_air_quality_api==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["greeclimate"],
|
||||
"requirements": ["greeclimate==2.1.0"]
|
||||
"requirements": ["greeclimate==2.1.1"]
|
||||
}
|
||||
|
||||
31
homeassistant/components/hdfury/__init__.py
Normal file
31
homeassistant/components/hdfury/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""The HDFury Integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.SELECT,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HDFuryConfigEntry) -> bool:
|
||||
"""Set up HDFury as config entry."""
|
||||
|
||||
coordinator = HDFuryCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HDFuryConfigEntry) -> bool:
|
||||
"""Unload a HDFury config entry."""
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
74
homeassistant/components/hdfury/button.py
Normal file
74
homeassistant/components/hdfury/button.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Button platform for HDFury Integration."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from hdfury import HDFuryAPI, HDFuryError
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HDFuryConfigEntry
|
||||
from .entity import HDFuryEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HDFuryButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Description for HDFury button entities."""
|
||||
|
||||
press_fn: Callable[[HDFuryAPI], Awaitable[None]]
|
||||
|
||||
|
||||
BUTTONS: tuple[HDFuryButtonEntityDescription, ...] = (
|
||||
HDFuryButtonEntityDescription(
|
||||
key="reboot",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda client: client.issue_reboot(),
|
||||
),
|
||||
HDFuryButtonEntityDescription(
|
||||
key="issue_hotplug",
|
||||
translation_key="issue_hotplug",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda client: client.issue_hotplug(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HDFuryConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up buttons using the platform schema."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
HDFuryButton(coordinator, description) for description in BUTTONS
|
||||
)
|
||||
|
||||
|
||||
class HDFuryButton(HDFuryEntity, ButtonEntity):
|
||||
"""HDFury Button Class."""
|
||||
|
||||
entity_description: HDFuryButtonEntityDescription
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle Button Press."""
|
||||
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator.client)
|
||||
except HDFuryError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from error
|
||||
54
homeassistant/components/hdfury/config_flow.py
Normal file
54
homeassistant/components/hdfury/config_flow.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Config flow for HDFury Integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from hdfury import HDFuryAPI, HDFuryError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class HDFuryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle Config Flow for HDFury."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle Initial Setup."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
|
||||
serial = await self._validate_connection(host)
|
||||
if serial is not None:
|
||||
await self.async_set_unique_id(serial)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"HDFury ({host})", data=user_input
|
||||
)
|
||||
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _validate_connection(self, host: str) -> str | None:
|
||||
"""Try to fetch serial number to confirm it's a valid HDFury device."""
|
||||
|
||||
client = HDFuryAPI(host, async_get_clientsession(self.hass))
|
||||
|
||||
try:
|
||||
data = await client.get_board()
|
||||
except HDFuryError:
|
||||
return None
|
||||
|
||||
return data["serial"]
|
||||
3
homeassistant/components/hdfury/const.py
Normal file
3
homeassistant/components/hdfury/const.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Constants for HDFury Integration."""
|
||||
|
||||
DOMAIN = "hdfury"
|
||||
67
homeassistant/components/hdfury/coordinator.py
Normal file
67
homeassistant/components/hdfury/coordinator.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""DataUpdateCoordinator for HDFury Integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from hdfury import HDFuryAPI, HDFuryError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=60)
|
||||
|
||||
type HDFuryConfigEntry = ConfigEntry[HDFuryCoordinator]
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HDFuryData:
|
||||
"""HDFury Data Class."""
|
||||
|
||||
board: dict[str, str]
|
||||
info: dict[str, str]
|
||||
config: dict[str, str]
|
||||
|
||||
|
||||
class HDFuryCoordinator(DataUpdateCoordinator[HDFuryData]):
|
||||
"""HDFury Device Coordinator Class."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: HDFuryConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name="HDFury",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.host: str = entry.data[CONF_HOST]
|
||||
self.client = HDFuryAPI(self.host, async_get_clientsession(hass))
|
||||
|
||||
async def _async_update_data(self) -> HDFuryData:
|
||||
"""Fetch the latest device data."""
|
||||
|
||||
try:
|
||||
board = await self.client.get_board()
|
||||
info = await self.client.get_info()
|
||||
config = await self.client.get_config()
|
||||
except HDFuryError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from error
|
||||
|
||||
return HDFuryData(
|
||||
board=board,
|
||||
info=info,
|
||||
config=config,
|
||||
)
|
||||
39
homeassistant/components/hdfury/entity.py
Normal file
39
homeassistant/components/hdfury/entity.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Base class for HDFury entities."""
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import HDFuryCoordinator
|
||||
|
||||
|
||||
class HDFuryEntity(CoordinatorEntity[HDFuryCoordinator]):
|
||||
"""Common elements for all entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, coordinator: HDFuryCoordinator, entity_description: EntityDescription
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.data.board['serial']}_{entity_description.key}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=f"HDFury {coordinator.data.board['hostname']}",
|
||||
manufacturer="HDFury",
|
||||
model=coordinator.data.board["hostname"].split("-")[0],
|
||||
serial_number=coordinator.data.board["serial"],
|
||||
sw_version=coordinator.data.board["version"].removeprefix("FW: "),
|
||||
hw_version=coordinator.data.board.get("pcbv"),
|
||||
configuration_url=f"http://{coordinator.host}",
|
||||
connections={
|
||||
(dr.CONNECTION_NETWORK_MAC, coordinator.data.config["macaddr"])
|
||||
},
|
||||
)
|
||||
52
homeassistant/components/hdfury/icons.json
Normal file
52
homeassistant/components/hdfury/icons.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"issue_hotplug": {
|
||||
"default": "mdi:connection"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"opmode": {
|
||||
"default": "mdi:cogs"
|
||||
},
|
||||
"portseltx0": {
|
||||
"default": "mdi:hdmi-port"
|
||||
},
|
||||
"portseltx1": {
|
||||
"default": "mdi:hdmi-port"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"autosw": {
|
||||
"default": "mdi:import"
|
||||
},
|
||||
"htpcmode0": {
|
||||
"default": "mdi:desktop-classic"
|
||||
},
|
||||
"htpcmode1": {
|
||||
"default": "mdi:desktop-classic"
|
||||
},
|
||||
"htpcmode2": {
|
||||
"default": "mdi:desktop-classic"
|
||||
},
|
||||
"htpcmode3": {
|
||||
"default": "mdi:desktop-classic"
|
||||
},
|
||||
"iractive": {
|
||||
"default": "mdi:remote"
|
||||
},
|
||||
"mutetx0": {
|
||||
"default": "mdi:volume-mute"
|
||||
},
|
||||
"mutetx1": {
|
||||
"default": "mdi:volume-mute"
|
||||
},
|
||||
"oled": {
|
||||
"default": "mdi:cellphone-information"
|
||||
},
|
||||
"relay": {
|
||||
"default": "mdi:electric-switch"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
homeassistant/components/hdfury/manifest.json
Normal file
11
homeassistant/components/hdfury/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "hdfury",
|
||||
"name": "HDFury",
|
||||
"codeowners": ["@glenndehaan"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hdfury",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["hdfury==1.3.1"]
|
||||
}
|
||||
74
homeassistant/components/hdfury/quality_scale.yaml
Normal file
74
homeassistant/components/hdfury/quality_scale.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Entities do not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration has no options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: Integration has no authentication flow.
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Device type integration.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Device type integration.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
122
homeassistant/components/hdfury/select.py
Normal file
122
homeassistant/components/hdfury/select.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Select platform for HDFury Integration."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from hdfury import (
|
||||
OPERATION_MODES,
|
||||
TX0_INPUT_PORTS,
|
||||
TX1_INPUT_PORTS,
|
||||
HDFuryAPI,
|
||||
HDFuryError,
|
||||
)
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
|
||||
from .entity import HDFuryEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HDFurySelectEntityDescription(SelectEntityDescription):
|
||||
"""Description for HDFury select entities."""
|
||||
|
||||
set_value_fn: Callable[[HDFuryAPI, str], Awaitable[None]]
|
||||
|
||||
|
||||
SELECT_PORTS: tuple[HDFurySelectEntityDescription, ...] = (
|
||||
HDFurySelectEntityDescription(
|
||||
key="portseltx0",
|
||||
translation_key="portseltx0",
|
||||
options=list(TX0_INPUT_PORTS.keys()),
|
||||
set_value_fn=lambda coordinator, value: _set_ports(coordinator),
|
||||
),
|
||||
HDFurySelectEntityDescription(
|
||||
key="portseltx1",
|
||||
translation_key="portseltx1",
|
||||
options=list(TX1_INPUT_PORTS.keys()),
|
||||
set_value_fn=lambda coordinator, value: _set_ports(coordinator),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SELECT_OPERATION_MODE: HDFurySelectEntityDescription = HDFurySelectEntityDescription(
|
||||
key="opmode",
|
||||
translation_key="opmode",
|
||||
options=list(OPERATION_MODES.keys()),
|
||||
set_value_fn=lambda coordinator, value: coordinator.client.set_operation_mode(
|
||||
value
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def _set_ports(coordinator: HDFuryCoordinator) -> None:
|
||||
tx0 = coordinator.data.info.get("portseltx0")
|
||||
tx1 = coordinator.data.info.get("portseltx1")
|
||||
|
||||
if tx0 is None or tx1 is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="tx_state_error",
|
||||
translation_placeholders={"details": f"tx0={tx0}, tx1={tx1}"},
|
||||
)
|
||||
|
||||
await coordinator.client.set_port_selection(tx0, tx1)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HDFuryConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up selects using the platform schema."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[HDFuryEntity] = []
|
||||
|
||||
for description in SELECT_PORTS:
|
||||
if description.key not in coordinator.data.info:
|
||||
continue
|
||||
|
||||
entities.append(HDFurySelect(coordinator, description))
|
||||
|
||||
# Add OPMODE select if present
|
||||
if "opmode" in coordinator.data.info:
|
||||
entities.append(HDFurySelect(coordinator, SELECT_OPERATION_MODE))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HDFurySelect(HDFuryEntity, SelectEntity):
|
||||
"""HDFury Select Class."""
|
||||
|
||||
entity_description: HDFurySelectEntityDescription
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
"""Return the current option."""
|
||||
|
||||
return self.coordinator.data.info[self.entity_description.key]
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Update the current option."""
|
||||
|
||||
# Update local data first
|
||||
self.coordinator.data.info[self.entity_description.key] = option
|
||||
|
||||
# Send command to device
|
||||
try:
|
||||
await self.entity_description.set_value_fn(self.coordinator, option)
|
||||
except HDFuryError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from error
|
||||
|
||||
# Trigger HA coordinator refresh
|
||||
await self.coordinator.async_request_refresh()
|
||||
101
homeassistant/components/hdfury/strings.json
Normal file
101
homeassistant/components/hdfury/strings.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your HDFury device."
|
||||
},
|
||||
"description": "Set up your HDFury to integrate with Home Assistant."
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"issue_hotplug": {
|
||||
"name": "Issue hotplug"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"opmode": {
|
||||
"name": "Operation mode",
|
||||
"state": {
|
||||
"0": "Mode 0 - Splitter TX0/TX1 FRL5 VRR",
|
||||
"1": "Mode 1 - Splitter TX0/TX1 UPSCALE FRL5",
|
||||
"2": "Mode 2 - Matrix TMDS",
|
||||
"3": "Mode 3 - Matrix FRL->TMDS",
|
||||
"4": "Mode 4 - Matrix DOWNSCALE",
|
||||
"5": "Mode 5 - Matrix RX0:FRL5 + RX1-3:TMDS"
|
||||
}
|
||||
},
|
||||
"portseltx0": {
|
||||
"name": "Port select TX0",
|
||||
"state": {
|
||||
"0": "Input 0",
|
||||
"1": "Input 1",
|
||||
"2": "Input 2",
|
||||
"3": "Input 3",
|
||||
"4": "Copy TX1"
|
||||
}
|
||||
},
|
||||
"portseltx1": {
|
||||
"name": "Port select TX1",
|
||||
"state": {
|
||||
"0": "Input 0",
|
||||
"1": "Input 1",
|
||||
"2": "Input 2",
|
||||
"3": "Input 3",
|
||||
"4": "Copy TX0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"autosw": {
|
||||
"name": "Auto switch inputs"
|
||||
},
|
||||
"htpcmode0": {
|
||||
"name": "HTPC mode RX0"
|
||||
},
|
||||
"htpcmode1": {
|
||||
"name": "HTPC mode RX1"
|
||||
},
|
||||
"htpcmode2": {
|
||||
"name": "HTPC mode RX2"
|
||||
},
|
||||
"htpcmode3": {
|
||||
"name": "HTPC mode RX3"
|
||||
},
|
||||
"iractive": {
|
||||
"name": "Infrared"
|
||||
},
|
||||
"mutetx0": {
|
||||
"name": "Mute audio TX0"
|
||||
},
|
||||
"mutetx1": {
|
||||
"name": "Mute audio TX1"
|
||||
},
|
||||
"oled": {
|
||||
"name": "OLED display"
|
||||
},
|
||||
"relay": {
|
||||
"name": "Relay"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with HDFury device"
|
||||
},
|
||||
"tx_state_error": {
|
||||
"message": "An error occurred while validating TX states: {details}"
|
||||
}
|
||||
}
|
||||
}
|
||||
142
homeassistant/components/hdfury/switch.py
Normal file
142
homeassistant/components/hdfury/switch.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""Switch platform for HDFury Integration."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from hdfury import HDFuryAPI, HDFuryError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HDFuryConfigEntry
|
||||
from .entity import HDFuryEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HDFurySwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Description for HDFury switch entities."""
|
||||
|
||||
set_value_fn: Callable[[HDFuryAPI, str], Awaitable[None]]
|
||||
|
||||
|
||||
SWITCHES: tuple[HDFurySwitchEntityDescription, ...] = (
|
||||
HDFurySwitchEntityDescription(
|
||||
key="autosw",
|
||||
translation_key="autosw",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_auto_switch_inputs(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="htpcmode0",
|
||||
translation_key="htpcmode0",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_htpc_mode_rx0(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="htpcmode1",
|
||||
translation_key="htpcmode1",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_htpc_mode_rx1(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="htpcmode2",
|
||||
translation_key="htpcmode2",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_htpc_mode_rx2(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="htpcmode3",
|
||||
translation_key="htpcmode3",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_htpc_mode_rx3(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="mutetx0",
|
||||
translation_key="mutetx0",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_mute_tx0_audio(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="mutetx1",
|
||||
translation_key="mutetx1",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_mute_tx1_audio(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="oled",
|
||||
translation_key="oled",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_oled(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="iractive",
|
||||
translation_key="iractive",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_ir_active(value),
|
||||
),
|
||||
HDFurySwitchEntityDescription(
|
||||
key="relay",
|
||||
translation_key="relay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=lambda client, value: client.set_relay(value),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HDFuryConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches using the platform schema."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
HDFurySwitch(coordinator, description)
|
||||
for description in SWITCHES
|
||||
if description.key in coordinator.data.config
|
||||
)
|
||||
|
||||
|
||||
class HDFurySwitch(HDFuryEntity, SwitchEntity):
|
||||
"""Base HDFury Switch Class."""
|
||||
|
||||
entity_description: HDFurySwitchEntityDescription
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Set Switch State."""
|
||||
|
||||
return self.coordinator.data.config.get(self.entity_description.key) == "1"
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Handle Switch On Event."""
|
||||
|
||||
try:
|
||||
await self.entity_description.set_value_fn(self.coordinator.client, "on")
|
||||
except HDFuryError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from error
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Handle Switch Off Event."""
|
||||
|
||||
try:
|
||||
await self.entity_description.set_value_fn(self.coordinator.client, "off")
|
||||
except HDFuryError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from error
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhik"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyHik==0.3.4"]
|
||||
"requirements": ["pyHik==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohomeconnect==0.26.0"],
|
||||
"requirements": ["aiohomeconnect==0.28.0"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ set_program_and_options:
|
||||
- active_program
|
||||
- selected_program
|
||||
program:
|
||||
example: dishcare_dishwasher_program_auto2
|
||||
example: dishcare_dishwasher_program_auto_2
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
@@ -73,6 +73,7 @@ set_program_and_options:
|
||||
- dishcare_dishwasher_program_intensiv_45
|
||||
- dishcare_dishwasher_program_auto_half_load
|
||||
- dishcare_dishwasher_program_intensiv_power
|
||||
- dishcare_dishwasher_program_intensive_fixed_zone
|
||||
- dishcare_dishwasher_program_magic_daily
|
||||
- dishcare_dishwasher_program_super_60
|
||||
- dishcare_dishwasher_program_kurz_60
|
||||
@@ -121,6 +122,7 @@ set_program_and_options:
|
||||
- cooking_oven_program_heating_mode_pre_heating
|
||||
- cooking_oven_program_heating_mode_hot_air
|
||||
- cooking_oven_program_heating_mode_hot_air_eco
|
||||
- cooking_oven_program_heating_mode_hot_air_gentle
|
||||
- cooking_oven_program_heating_mode_hot_air_grilling
|
||||
- cooking_oven_program_heating_mode_top_bottom_heating
|
||||
- cooking_oven_program_heating_mode_top_bottom_heating_eco
|
||||
@@ -147,6 +149,7 @@ set_program_and_options:
|
||||
- cooking_oven_program_microwave_900_watt
|
||||
- cooking_oven_program_microwave_1000_watt
|
||||
- cooking_oven_program_microwave_max
|
||||
- cooking_oven_program_steam_modes_steam
|
||||
- cooking_oven_program_heating_mode_warming_drawer
|
||||
- laundry_care_washer_program_auto_30
|
||||
- laundry_care_washer_program_auto_40
|
||||
@@ -174,7 +177,7 @@ set_program_and_options:
|
||||
- laundry_care_washer_program_rinse_rinse_spin_drain
|
||||
- laundry_care_washer_program_sensitive
|
||||
- laundry_care_washer_program_shirts_blouses
|
||||
- laundry_care_washer_program_spin_drain
|
||||
- laundry_care_washer_program_spin_spin_drain
|
||||
- laundry_care_washer_program_sport_fitness
|
||||
- laundry_care_washer_program_super_153045_super_15
|
||||
- laundry_care_washer_program_super_153045_super_1530
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
"cooking_oven_program_heating_mode_hot_air_60_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_60_steam%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_80_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_80_steam%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_eco": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_eco%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_gentle": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_gentle%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_grilling%]",
|
||||
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_intensive_heat%]",
|
||||
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_keep_warm%]",
|
||||
@@ -271,6 +272,7 @@
|
||||
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
|
||||
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
|
||||
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
|
||||
"dishcare_dishwasher_program_intensive_fixed_zone": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensive_fixed_zone%]",
|
||||
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_kurz_60%]",
|
||||
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
|
||||
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
|
||||
@@ -350,7 +352,7 @@
|
||||
"laundry_care_washer_program_rinse_rinse_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_rinse_rinse_spin_drain%]",
|
||||
"laundry_care_washer_program_sensitive": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sensitive%]",
|
||||
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_shirts_blouses%]",
|
||||
"laundry_care_washer_program_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_drain%]",
|
||||
"laundry_care_washer_program_spin_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
|
||||
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_fitness%]",
|
||||
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_15%]",
|
||||
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_1530%]",
|
||||
@@ -592,6 +594,7 @@
|
||||
"cooking_oven_program_heating_mode_hot_air_60_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_60_steam%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_80_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_80_steam%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_eco": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_eco%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_gentle": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_gentle%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_grilling%]",
|
||||
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_intensive_heat%]",
|
||||
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_keep_warm%]",
|
||||
@@ -612,6 +615,7 @@
|
||||
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_900_watt%]",
|
||||
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_90_watt%]",
|
||||
"cooking_oven_program_microwave_max": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_max%]",
|
||||
"cooking_oven_program_steam_modes_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_steam%]",
|
||||
"dishcare_dishwasher_program_auto_1": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_1%]",
|
||||
"dishcare_dishwasher_program_auto_2": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_2%]",
|
||||
"dishcare_dishwasher_program_auto_3": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_3%]",
|
||||
@@ -623,6 +627,7 @@
|
||||
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
|
||||
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
|
||||
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
|
||||
"dishcare_dishwasher_program_intensive_fixed_zone": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensive_fixed_zone%]",
|
||||
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_kurz_60%]",
|
||||
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
|
||||
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
|
||||
@@ -702,7 +707,7 @@
|
||||
"laundry_care_washer_program_rinse_rinse_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_rinse_rinse_spin_drain%]",
|
||||
"laundry_care_washer_program_sensitive": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sensitive%]",
|
||||
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_shirts_blouses%]",
|
||||
"laundry_care_washer_program_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_drain%]",
|
||||
"laundry_care_washer_program_spin_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
|
||||
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_fitness%]",
|
||||
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_15%]",
|
||||
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_1530%]",
|
||||
@@ -1583,6 +1588,7 @@
|
||||
"cooking_oven_program_heating_mode_hot_air_60_steam": "Hot air + 60 RH",
|
||||
"cooking_oven_program_heating_mode_hot_air_80_steam": "Hot air + 80 RH",
|
||||
"cooking_oven_program_heating_mode_hot_air_eco": "Hot air eco",
|
||||
"cooking_oven_program_heating_mode_hot_air_gentle": "Hot air gentle",
|
||||
"cooking_oven_program_heating_mode_hot_air_grilling": "Hot air grilling",
|
||||
"cooking_oven_program_heating_mode_intensive_heat": "Intensive heat",
|
||||
"cooking_oven_program_heating_mode_keep_warm": "Keep warm",
|
||||
@@ -1603,6 +1609,7 @@
|
||||
"cooking_oven_program_microwave_900_watt": "900 Watt",
|
||||
"cooking_oven_program_microwave_90_watt": "90 Watt",
|
||||
"cooking_oven_program_microwave_max": "Max",
|
||||
"cooking_oven_program_steam_modes_steam": "Steam mode",
|
||||
"dishcare_dishwasher_program_auto_1": "Auto 1",
|
||||
"dishcare_dishwasher_program_auto_2": "Auto 2",
|
||||
"dishcare_dishwasher_program_auto_3": "Auto 3",
|
||||
@@ -1614,6 +1621,7 @@
|
||||
"dishcare_dishwasher_program_intensiv_45": "Intensive 45ºC",
|
||||
"dishcare_dishwasher_program_intensiv_70": "Intensive 70ºC",
|
||||
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
|
||||
"dishcare_dishwasher_program_intensive_fixed_zone": "Intensive fixed zone",
|
||||
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
|
||||
"dishcare_dishwasher_program_learning_dishwasher": "Intelligent",
|
||||
"dishcare_dishwasher_program_machine_care": "Machine care",
|
||||
@@ -1693,7 +1701,7 @@
|
||||
"laundry_care_washer_program_rinse_rinse_spin_drain": "Rinse spin drain",
|
||||
"laundry_care_washer_program_sensitive": "Sensitive",
|
||||
"laundry_care_washer_program_shirts_blouses": "Shirts/blouses",
|
||||
"laundry_care_washer_program_spin_drain": "Spin/drain",
|
||||
"laundry_care_washer_program_spin_spin_drain": "Spin/drain",
|
||||
"laundry_care_washer_program_sport_fitness": "Sport/fitness",
|
||||
"laundry_care_washer_program_super_153045_super_15": "Super 15 min",
|
||||
"laundry_care_washer_program_super_153045_super_1530": "Super 15/30 min",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"serialx==0.5.0",
|
||||
"serialx==0.6.2",
|
||||
"universal-silabs-flasher==0.1.2",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
|
||||
@@ -34,6 +34,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_TARGET_HUMIDITY_STEP,
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DOMAIN,
|
||||
@@ -141,6 +142,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||
"min_humidity",
|
||||
"max_humidity",
|
||||
"supported_features",
|
||||
"target_humidity_step",
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +150,12 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
|
||||
"""Base class for humidifier entities."""
|
||||
|
||||
_entity_component_unrecorded_attributes = frozenset(
|
||||
{ATTR_MIN_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_AVAILABLE_MODES}
|
||||
{
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_AVAILABLE_MODES,
|
||||
ATTR_TARGET_HUMIDITY_STEP,
|
||||
}
|
||||
)
|
||||
|
||||
entity_description: HumidifierEntityDescription
|
||||
@@ -161,6 +168,7 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
|
||||
_attr_mode: str | None
|
||||
_attr_supported_features: HumidifierEntityFeature = HumidifierEntityFeature(0)
|
||||
_attr_target_humidity: float | None = None
|
||||
_attr_target_humidity_step: float | None = None
|
||||
|
||||
@property
|
||||
def capability_attributes(self) -> dict[str, Any]:
|
||||
@@ -169,6 +177,8 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
|
||||
ATTR_MIN_HUMIDITY: self.min_humidity,
|
||||
ATTR_MAX_HUMIDITY: self.max_humidity,
|
||||
}
|
||||
if self.target_humidity_step is not None:
|
||||
data[ATTR_TARGET_HUMIDITY_STEP] = self.target_humidity_step
|
||||
|
||||
if HumidifierEntityFeature.MODES in self.supported_features:
|
||||
data[ATTR_AVAILABLE_MODES] = self.available_modes
|
||||
@@ -251,6 +261,11 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
|
||||
"""Set new mode."""
|
||||
await self.hass.async_add_executor_job(self.set_mode, mode)
|
||||
|
||||
@cached_property
|
||||
def target_humidity_step(self) -> float | None:
|
||||
"""Return the supported step of humidity."""
|
||||
return self._attr_target_humidity_step
|
||||
|
||||
@cached_property
|
||||
def min_humidity(self) -> float:
|
||||
"""Return the minimum humidity."""
|
||||
|
||||
@@ -28,6 +28,7 @@ ATTR_CURRENT_HUMIDITY = "current_humidity"
|
||||
ATTR_HUMIDITY = "humidity"
|
||||
ATTR_MAX_HUMIDITY = "max_humidity"
|
||||
ATTR_MIN_HUMIDITY = "min_humidity"
|
||||
ATTR_TARGET_HUMIDITY_STEP = "target_humidity_step"
|
||||
|
||||
DEFAULT_MIN_HUMIDITY = 0
|
||||
DEFAULT_MAX_HUMIDITY = 100
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,14 +31,11 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["incomfortclient"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["incomfort-client==0.6.10"]
|
||||
"requirements": ["incomfort-client==0.6.11"]
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
Platform.EVENT,
|
||||
Platform.FAN,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
|
||||
195
homeassistant/components/lg_thinq/humidifier.py
Normal file
195
homeassistant/components/lg_thinq/humidifier.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""Support for humidifier entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from thinqconnect import DeviceType
|
||||
from thinqconnect.devices.const import Property as ThinQProperty
|
||||
from thinqconnect.integration import ActiveMode
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
HumidifierAction,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntity,
|
||||
HumidifierEntityDescription,
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import ThinqConfigEntry
|
||||
from .coordinator import DeviceDataUpdateCoordinator
|
||||
from .entity import ThinQEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ThinQHumidifierEntityDescription(HumidifierEntityDescription):
|
||||
"""Describes ThinQ humidifier entity."""
|
||||
|
||||
current_humidity_key: str
|
||||
operation_key: str
|
||||
mode_key: str = ThinQProperty.CURRENT_JOB_MODE
|
||||
|
||||
|
||||
DEVICE_TYPE_HUM_MAP: dict[DeviceType, ThinQHumidifierEntityDescription] = {
|
||||
DeviceType.DEHUMIDIFIER: ThinQHumidifierEntityDescription(
|
||||
key=ThinQProperty.TARGET_HUMIDITY,
|
||||
name=None,
|
||||
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
|
||||
translation_key="dehumidifier",
|
||||
current_humidity_key=ThinQProperty.CURRENT_HUMIDITY,
|
||||
operation_key=ThinQProperty.DEHUMIDIFIER_OPERATION_MODE,
|
||||
),
|
||||
DeviceType.HUMIDIFIER: ThinQHumidifierEntityDescription(
|
||||
key=ThinQProperty.TARGET_HUMIDITY,
|
||||
name=None,
|
||||
device_class=HumidifierDeviceClass.HUMIDIFIER,
|
||||
translation_key="humidifier",
|
||||
current_humidity_key=ThinQProperty.HUMIDITY,
|
||||
operation_key=ThinQProperty.HUMIDIFIER_OPERATION_MODE,
|
||||
),
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ThinqConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up an entry for humidifier platform."""
|
||||
entities: list[ThinQHumidifierEntity] = []
|
||||
for coordinator in entry.runtime_data.coordinators.values():
|
||||
if (
|
||||
description := DEVICE_TYPE_HUM_MAP.get(coordinator.api.device.device_type)
|
||||
) is not None:
|
||||
entities.extend(
|
||||
ThinQHumidifierEntity(coordinator, description, property_id)
|
||||
for property_id in coordinator.api.get_active_idx(
|
||||
description.key, ActiveMode.READ_WRITE
|
||||
)
|
||||
)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ThinQHumidifierEntity(ThinQEntity, HumidifierEntity):
|
||||
"""Represent a ThinQ humidifier entity."""
|
||||
|
||||
entity_description: ThinQHumidifierEntityDescription
|
||||
_attr_supported_features = HumidifierEntityFeature.MODES
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DeviceDataUpdateCoordinator,
|
||||
entity_description: ThinQHumidifierEntityDescription,
|
||||
property_id: str,
|
||||
) -> None:
|
||||
"""Initialize a humidifier entity."""
|
||||
super().__init__(coordinator, entity_description, property_id)
|
||||
self._attr_available_modes = self.coordinator.data[
|
||||
self.entity_description.mode_key
|
||||
].options
|
||||
|
||||
if self.data.max is not None:
|
||||
self._attr_max_humidity = self.data.max
|
||||
if self.data.min is not None:
|
||||
self._attr_min_humidity = self.data.min
|
||||
self._attr_target_humidity_step = (
|
||||
self.data.step if self.data.step is not None else 1
|
||||
)
|
||||
|
||||
def _update_status(self) -> None:
|
||||
"""Update status itself."""
|
||||
super()._update_status()
|
||||
|
||||
self._attr_target_humidity = self.data.value
|
||||
self._attr_current_humidity = self.coordinator.data[
|
||||
self.entity_description.current_humidity_key
|
||||
].value
|
||||
self._attr_is_on = self.coordinator.data[
|
||||
self.entity_description.operation_key
|
||||
].is_on
|
||||
self._attr_mode = self.coordinator.data[self.entity_description.mode_key].value
|
||||
if self.is_on:
|
||||
self._attr_action = (
|
||||
HumidifierAction.DRYING
|
||||
if self.entity_description.device_class
|
||||
== HumidifierDeviceClass.DEHUMIDIFIER
|
||||
else HumidifierAction.HUMIDIFYING
|
||||
)
|
||||
else:
|
||||
self._attr_action = HumidifierAction.OFF
|
||||
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] update status: c:%s, t:%s, mode:%s, action:%s, is_on:%s",
|
||||
self.coordinator.device_name,
|
||||
self.property_id,
|
||||
self.current_humidity,
|
||||
self.target_humidity,
|
||||
self.mode,
|
||||
self.action,
|
||||
self.is_on,
|
||||
)
|
||||
|
||||
async def async_set_mode(self, mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] async_set_mode: %s",
|
||||
self.coordinator.device_name,
|
||||
self.entity_description.mode_key,
|
||||
mode,
|
||||
)
|
||||
await self.async_call_api(
|
||||
self.coordinator.api.post(self.entity_description.mode_key, mode)
|
||||
)
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
_target_humidity = round(humidity / (self.target_humidity_step or 1)) * (
|
||||
self.target_humidity_step or 1
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] async_set_humidity: %s, target_humidity: %s, step: %s",
|
||||
self.coordinator.device_name,
|
||||
self.property_id,
|
||||
humidity,
|
||||
_target_humidity,
|
||||
self.target_humidity_step,
|
||||
)
|
||||
if _target_humidity == self.target_humidity:
|
||||
return
|
||||
await self.async_call_api(
|
||||
self.coordinator.api.post(self.property_id, _target_humidity)
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
if self.is_on:
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] async_turn_on",
|
||||
self.coordinator.device_name,
|
||||
self.entity_description.operation_key,
|
||||
)
|
||||
await self.async_call_api(
|
||||
self.coordinator.api.async_turn_on(self.entity_description.operation_key)
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
if not self.is_on:
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] async_turn_off",
|
||||
self.coordinator.device_name,
|
||||
self.entity_description.operation_key,
|
||||
)
|
||||
await self.async_call_api(
|
||||
self.coordinator.api.async_turn_off(self.entity_description.operation_key)
|
||||
)
|
||||
@@ -199,6 +199,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"dehumidifier": {
|
||||
"state_attributes": {
|
||||
"mode": {
|
||||
"state": {
|
||||
"air_clean": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::air_clean%]",
|
||||
"clothes_dry": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::clothes_dry%]",
|
||||
"intensive_dry": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::intensive_dry%]",
|
||||
"quiet_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::quiet_humidity%]",
|
||||
"rapid_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::rapid_humidity%]",
|
||||
"smart_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::smart_humidity%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"state_attributes": {
|
||||
"mode": {
|
||||
"state": {
|
||||
"air_clean": "[%key:component::lg_thinq::entity::select::current_job_mode::state::air_clean%]",
|
||||
"humidify": "[%key:component::lg_thinq::entity::select::current_job_mode::state::humidify%]",
|
||||
"humidify_and_air_clean": "[%key:component::lg_thinq::entity::select::current_job_mode::state::humidify_and_air_clean%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"fan_speed": {
|
||||
"name": "Fan"
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,10 +31,6 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
turned_on: *trigger_common
|
||||
@@ -48,6 +48,7 @@ brightness_crossed_threshold:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type:
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "namecheapdns",
|
||||
"name": "Namecheap FreeDNS",
|
||||
"name": "Namecheap DynamicDNS",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/namecheapdns",
|
||||
"iot_class": "cloud_push",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Support for Netatmo binary sensors."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Final, cast
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
@@ -9,17 +13,33 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import NETATMO_CREATE_WEATHER_SENSOR
|
||||
from .const import NETATMO_CREATE_WEATHER_BINARY_SENSOR
|
||||
from .data_handler import NetatmoDevice
|
||||
from .entity import NetatmoWeatherModuleEntity
|
||||
|
||||
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
BinarySensorEntityDescription(
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NetatmoBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes Netatmo binary sensor entity."""
|
||||
|
||||
name: str | None = None # The default name of the sensor
|
||||
netatmo_name: str # The name used by Netatmo API for this sensor
|
||||
|
||||
|
||||
NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS: Final[
|
||||
list[NetatmoBinarySensorEntityDescription]
|
||||
] = [
|
||||
NetatmoBinarySensorEntityDescription(
|
||||
key="reachable",
|
||||
name="Connectivity",
|
||||
netatmo_name="reachable",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -27,36 +47,75 @@ async def async_setup_entry(
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Netatmo binary sensors based on a config entry."""
|
||||
"""Set up Netatmo weather binary sensors based on a config entry."""
|
||||
|
||||
@callback
|
||||
def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
async_add_entities(
|
||||
NetatmoWeatherBinarySensor(netatmo_device, description)
|
||||
for description in BINARY_SENSOR_TYPES
|
||||
if description.key in netatmo_device.device.features
|
||||
)
|
||||
"""Create weather binary sensor entities for a Netatmo weather device."""
|
||||
|
||||
descriptions_to_add = NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS
|
||||
|
||||
entities: list[NetatmoWeatherBinarySensor] = []
|
||||
|
||||
# Create binary sensors for module
|
||||
for description in descriptions_to_add:
|
||||
# Actual check is simple for reachable
|
||||
feature_check = description.key
|
||||
if feature_check in netatmo_device.device.features:
|
||||
_LOGGER.debug(
|
||||
'Adding "%s" weather binary sensor for device %s',
|
||||
feature_check,
|
||||
netatmo_device.device.name,
|
||||
)
|
||||
entities.append(
|
||||
NetatmoWeatherBinarySensor(
|
||||
netatmo_device,
|
||||
description,
|
||||
)
|
||||
)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_binary_sensor_entity
|
||||
hass,
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
_create_weather_binary_sensor_entity,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity):
|
||||
"""Implementation of a Netatmo binary sensor."""
|
||||
"""Implementation of a Netatmo weather binary sensor."""
|
||||
|
||||
entity_description: NetatmoBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self, device: NetatmoDevice, description: BinarySensorEntityDescription
|
||||
self,
|
||||
netatmo_device: NetatmoDevice,
|
||||
description: NetatmoBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Netatmo binary sensor."""
|
||||
super().__init__(device)
|
||||
"""Initialize a Netatmo weather binary sensor."""
|
||||
|
||||
super().__init__(netatmo_device)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self.device.entity_id}-{description.key}"
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
self._attr_is_on = self.device.reachable
|
||||
|
||||
value: StateType | None = None
|
||||
|
||||
value = getattr(self.device, self.entity_description.netatmo_name, None)
|
||||
|
||||
if value is None:
|
||||
self._attr_available = False
|
||||
self._attr_is_on = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._attr_is_on = cast(bool, value)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -53,6 +53,7 @@ NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor"
|
||||
NETATMO_CREATE_SELECT = "netatmo_create_select"
|
||||
NETATMO_CREATE_SENSOR = "netatmo_create_sensor"
|
||||
NETATMO_CREATE_SWITCH = "netatmo_create_switch"
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR = "netatmo_create_weather_binary_sensor"
|
||||
NETATMO_CREATE_WEATHER_SENSOR = "netatmo_create_weather_sensor"
|
||||
|
||||
CONF_AREA_NAME = "area_name"
|
||||
|
||||
@@ -45,6 +45,7 @@ from .const import (
|
||||
NETATMO_CREATE_SELECT,
|
||||
NETATMO_CREATE_SENSOR,
|
||||
NETATMO_CREATE_SWITCH,
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
PLATFORMS,
|
||||
WEBHOOK_ACTIVATION,
|
||||
@@ -332,16 +333,20 @@ class NetatmoDataHandler:
|
||||
"""Set up home coach/air care modules."""
|
||||
for module in self.account.modules.values():
|
||||
if module.device_category is NetatmoDeviceCategory.air_care:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
for signal in (
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
AIR_CARE,
|
||||
AIR_CARE,
|
||||
),
|
||||
)
|
||||
):
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
signal,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
AIR_CARE,
|
||||
AIR_CARE,
|
||||
),
|
||||
)
|
||||
|
||||
def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None:
|
||||
"""Set up modules."""
|
||||
@@ -379,16 +384,20 @@ class NetatmoDataHandler:
|
||||
),
|
||||
)
|
||||
if module.device_category is NetatmoDeviceCategory.weather:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
for signal in (
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
home.entity_id,
|
||||
WEATHER,
|
||||
),
|
||||
)
|
||||
):
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
signal,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
home.entity_id,
|
||||
WEATHER,
|
||||
),
|
||||
)
|
||||
|
||||
def setup_rooms(self, home: pyatmo.Home, signal_home: str) -> None:
|
||||
"""Set up rooms."""
|
||||
|
||||
@@ -256,7 +256,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
if slot_id > user_input[CONF_MESSAGE_SLOTS]
|
||||
]
|
||||
|
||||
removed_entites_area = [
|
||||
removed_entities_area = [
|
||||
f"{cfg_region}-{slot_id}"
|
||||
for slot_id in range(1, self.data[CONF_MESSAGE_SLOTS] + 1)
|
||||
for cfg_region in self.data[CONF_REGIONS]
|
||||
@@ -265,7 +265,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
|
||||
for entry in entries:
|
||||
for entity_uid in list(
|
||||
set(removed_entities_slots + removed_entites_area)
|
||||
set(removed_entities_slots + removed_entities_area)
|
||||
):
|
||||
if entry.unique_id == entity_uid:
|
||||
entity_registry.async_remove(entry.entity_id)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynina"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynina==0.3.6"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
103
homeassistant/components/nina/quality_scale.yaml
Normal file
103
homeassistant/components/nina/quality_scale.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration does not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use services that need authentication.
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Use load_json_object_fixture in tests
|
||||
Patch the library instead of the HTTP requests
|
||||
Create a shared fixture for the mock config entry
|
||||
Use init_integration in tests
|
||||
Evaluate the need of test_config_entry_not_ready
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does use a cloud service.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does use a cloud service.
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use devices.
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: done
|
||||
entity-category: todo
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: |
|
||||
Extract attributes into own entities.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not custom icons.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use issues.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use devices.
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoauth", "pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.0"]
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.2"]
|
||||
}
|
||||
|
||||
@@ -28,9 +28,15 @@
|
||||
"exchange_rate": {
|
||||
"default": "mdi:currency-usd"
|
||||
},
|
||||
"highest_price": {
|
||||
"default": "mdi:cash-plus"
|
||||
},
|
||||
"last_price": {
|
||||
"default": "mdi:cash"
|
||||
},
|
||||
"lowest_price": {
|
||||
"default": "mdi:cash-minus"
|
||||
},
|
||||
"next_price": {
|
||||
"default": "mdi:cash"
|
||||
},
|
||||
|
||||
@@ -6,9 +6,10 @@ from openevsehttp.__main__ import OpenEVSE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.helpers.service_info import zeroconf
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_ID, CONF_SERIAL, DOMAIN
|
||||
|
||||
|
||||
class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@@ -17,27 +18,33 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def check_status(self, host: str) -> bool:
|
||||
def __init__(self) -> None:
|
||||
"""Set up the instance."""
|
||||
self.discovery_info: dict[str, Any] = {}
|
||||
|
||||
async def check_status(self, host: str) -> tuple[bool, str | None]:
|
||||
"""Check if we can connect to the OpenEVSE charger."""
|
||||
|
||||
charger = OpenEVSE(host)
|
||||
try:
|
||||
await charger.test_and_get()
|
||||
result = await charger.test_and_get()
|
||||
except TimeoutError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False, None
|
||||
return True, result.get(CONF_SERIAL)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors = None
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
if await self.check_status(user_input[CONF_HOST]):
|
||||
if (result := await self.check_status(user_input[CONF_HOST]))[0]:
|
||||
if (serial := result[1]) is not None:
|
||||
await self.async_set_unique_id(serial, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"OpenEVSE {user_input[CONF_HOST]}",
|
||||
data=user_input,
|
||||
@@ -55,10 +62,53 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._async_abort_entries_match({CONF_HOST: data[CONF_HOST]})
|
||||
|
||||
if not await self.check_status(data[CONF_HOST]):
|
||||
if (result := await self.check_status(data[CONF_HOST]))[0]:
|
||||
if (serial := result[1]) is not None:
|
||||
await self.async_set_unique_id(serial)
|
||||
self._abort_if_unique_id_configured()
|
||||
else:
|
||||
return self.async_abort(reason="unavailable_host")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"OpenEVSE {data[CONF_HOST]}",
|
||||
data=data,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
self._async_abort_entries_match({CONF_HOST: discovery_info.host})
|
||||
|
||||
await self.async_set_unique_id(discovery_info.properties[CONF_ID])
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
|
||||
|
||||
host = discovery_info.host
|
||||
name = f"OpenEVSE {discovery_info.name.split('.')[0]}"
|
||||
self.discovery_info.update(
|
||||
{
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
}
|
||||
)
|
||||
self.context.update({"title_placeholders": {"name": name}})
|
||||
|
||||
if not (await self.check_status(host))[0]:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={"name": self.discovery_info[CONF_NAME]},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.discovery_info[CONF_NAME],
|
||||
data={CONF_HOST: self.discovery_info[CONF_HOST]},
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Constants for the OpenEVSE integration."""
|
||||
|
||||
CONF_ID = "id"
|
||||
CONF_SERIAL = "serial"
|
||||
DOMAIN = "openevse"
|
||||
INTEGRATION_TITLE = "OpenEVSE"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"domain": "openevse",
|
||||
"name": "OpenEVSE",
|
||||
"codeowners": ["@c00w"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@c00w", "@firstof9"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/openevse",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openevsehttp"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["python-openevse-http==0.2.1"]
|
||||
"requirements": ["python-openevse-http==0.2.1"],
|
||||
"zeroconf": ["_openevse._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
ATTR_SERIAL_NUMBER,
|
||||
CONF_HOST,
|
||||
CONF_MONITORED_VARIABLES,
|
||||
UnitOfEnergy,
|
||||
@@ -26,6 +28,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@@ -40,25 +43,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="status",
|
||||
name="Charging Status",
|
||||
translation_key="status",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="charge_time",
|
||||
name="Charge Time Elapsed",
|
||||
translation_key="charge_time",
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="ambient_temp",
|
||||
name="Ambient Temperature",
|
||||
translation_key="ambient_temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="ir_temp",
|
||||
name="IR Temperature",
|
||||
translation_key="ir_temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -66,7 +69,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="rtc_temp",
|
||||
name="RTC Temperature",
|
||||
translation_key="rtc_temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -74,14 +77,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="usage_session",
|
||||
name="Usage this Session",
|
||||
translation_key="usage_session",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="usage_total",
|
||||
name="Total Usage",
|
||||
translation_key="usage_total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@@ -158,9 +161,10 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
(
|
||||
OpenEVSESensor(
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.runtime_data,
|
||||
description,
|
||||
config_entry.entry_id,
|
||||
config_entry.unique_id,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
),
|
||||
@@ -171,17 +175,32 @@ async def async_setup_entry(
|
||||
class OpenEVSESensor(SensorEntity):
|
||||
"""Implementation of an OpenEVSE sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
charger: OpenEVSE,
|
||||
description: SensorEntityDescription,
|
||||
entry_id: str,
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self.host = host
|
||||
self.charger = charger
|
||||
|
||||
identifier = unique_id or entry_id
|
||||
self._attr_unique_id = f"{identifier}-{description.key}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, identifier)},
|
||||
manufacturer="OpenEVSE",
|
||||
)
|
||||
if unique_id:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(CONNECTION_NETWORK_MAC, unique_id)
|
||||
}
|
||||
self._attr_device_info[ATTR_SERIAL_NUMBER] = unique_id
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the monitored data from the charger."""
|
||||
try:
|
||||
|
||||
@@ -18,6 +18,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ambient_temp": {
|
||||
"name": "Ambient temperature"
|
||||
},
|
||||
"charge_time": {
|
||||
"name": "Charge time elapsed"
|
||||
},
|
||||
"ir_temp": {
|
||||
"name": "IR temperature"
|
||||
},
|
||||
"rtc_temp": {
|
||||
"name": "RTC temperature"
|
||||
},
|
||||
"status": {
|
||||
"name": "Charging status"
|
||||
},
|
||||
"usage_session": {
|
||||
"name": "Usage this session"
|
||||
},
|
||||
"usage_total": {
|
||||
"name": "Total energy usage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"yaml_deprecated": {
|
||||
"description": "Configuring OpenEVSE using YAML is being removed. Your existing YAML configuration has been imported into the UI automatically. Remove the `openevse` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.16.0"]
|
||||
"requirements": ["opower==0.16.1"]
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = {
|
||||
key="optimization_mode",
|
||||
translation_key="optimization_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced"],
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced", "nettleie"],
|
||||
value_fn=lambda entity_data: entity_data.state.lower(),
|
||||
),
|
||||
"power_load": OSOEnergySensorEntityDescription(
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"state": {
|
||||
"advanced": "Advanced",
|
||||
"gridcompany": "Grid company",
|
||||
"nettleie": "Grid fee",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"oso": "OSO",
|
||||
"smartcompany": "Smart company"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.7.0"]
|
||||
"requirements": ["python-otbr-api==2.7.1"]
|
||||
}
|
||||
|
||||
@@ -128,15 +128,15 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
|
||||
|
||||
states = self.device.states
|
||||
|
||||
if (
|
||||
operating_mode := states[OverkizState.CORE_OPERATING_MODE]
|
||||
) and operating_mode.value_as_str == OverkizCommandParam.EXTERNAL:
|
||||
return PRESET_EXTERNAL
|
||||
|
||||
if (
|
||||
state := states[OverkizState.IO_TARGET_HEATING_LEVEL]
|
||||
) and state.value_as_str:
|
||||
return OVERKIZ_TO_PRESET_MODE[state.value_as_str]
|
||||
|
||||
if (
|
||||
operating_mode := states[OverkizState.CORE_OPERATING_MODE]
|
||||
) and operating_mode.value_as_str == OverkizCommandParam.EXTERNAL:
|
||||
return PRESET_EXTERNAL
|
||||
return None
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.19.3"],
|
||||
"requirements": ["pyoverkiz==1.19.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "gateway*",
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyportainer import Portainer
|
||||
@@ -30,23 +33,40 @@ from .coordinator import (
|
||||
PortainerCoordinator,
|
||||
PortainerCoordinatorData,
|
||||
)
|
||||
from .entity import PortainerContainerEntity
|
||||
from .entity import PortainerContainerEntity, PortainerEndpointEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PortainerButtonDescription(ButtonEntityDescription):
|
||||
"""Class to describe a Portainer button entity."""
|
||||
|
||||
# Note to reviewer: I am keeping the third argument a str, in order to keep mypy happy :)
|
||||
press_action: Callable[
|
||||
[Portainer, int, str],
|
||||
Coroutine[Any, Any, None],
|
||||
]
|
||||
|
||||
|
||||
BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
ENDPOINT_BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
PortainerButtonDescription(
|
||||
key="images_prune",
|
||||
translation_key="images_prune",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action=(
|
||||
lambda portainer, endpoint_id, _: portainer.images_prune(
|
||||
endpoint_id=endpoint_id, dangling=False, until=timedelta(days=0)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
CONTAINER_BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
PortainerButtonDescription(
|
||||
key="restart",
|
||||
name="Restart Container",
|
||||
translation_key="restart_container",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action=(
|
||||
@@ -66,22 +86,43 @@ async def async_setup_entry(
|
||||
"""Set up Portainer buttons."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def _async_add_new_endpoints(endpoints: list[PortainerCoordinatorData]) -> None:
|
||||
"""Add new endpoint binary sensors."""
|
||||
async_add_entities(
|
||||
PortainerEndpointButton(
|
||||
coordinator,
|
||||
entity_description,
|
||||
endpoint,
|
||||
)
|
||||
for entity_description in ENDPOINT_BUTTONS
|
||||
for endpoint in endpoints
|
||||
)
|
||||
|
||||
def _async_add_new_containers(
|
||||
containers: list[tuple[PortainerCoordinatorData, PortainerContainerData]],
|
||||
) -> None:
|
||||
"""Add new container button sensors."""
|
||||
async_add_entities(
|
||||
PortainerButton(
|
||||
PortainerContainerButton(
|
||||
coordinator,
|
||||
entity_description,
|
||||
container,
|
||||
endpoint,
|
||||
)
|
||||
for (endpoint, container) in containers
|
||||
for entity_description in BUTTONS
|
||||
for entity_description in CONTAINER_BUTTONS
|
||||
)
|
||||
|
||||
coordinator.new_endpoints_callbacks.append(_async_add_new_endpoints)
|
||||
coordinator.new_containers_callbacks.append(_async_add_new_containers)
|
||||
|
||||
_async_add_new_endpoints(
|
||||
[
|
||||
endpoint
|
||||
for endpoint in coordinator.data.values()
|
||||
if endpoint.id in coordinator.known_endpoints
|
||||
]
|
||||
)
|
||||
_async_add_new_containers(
|
||||
[
|
||||
(endpoint, container)
|
||||
@@ -91,7 +132,62 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class PortainerButton(PortainerContainerEntity, ButtonEntity):
|
||||
class PortainerBaseButton(ButtonEntity):
|
||||
"""Common base for Portainer buttons. Basically to ensure the async_press logic isn't duplicated."""
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
coordinator: PortainerCoordinator
|
||||
|
||||
@abstractmethod
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Abstract method used per Portainer button class."""
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger the Portainer button press service."""
|
||||
try:
|
||||
await self._async_press_call()
|
||||
except PortainerConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect_no_details",
|
||||
) from err
|
||||
except PortainerAuthenticationError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth_no_details",
|
||||
) from err
|
||||
except PortainerTimeoutError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_connect_no_details",
|
||||
) from err
|
||||
|
||||
|
||||
class PortainerEndpointButton(PortainerEndpointEntity, PortainerBaseButton):
|
||||
"""Defines a Portainer endpoint button."""
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerButtonDescription,
|
||||
device_info: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer endpoint button entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Call the endpoint button press action."""
|
||||
await self.entity_description.press_action(
|
||||
self.coordinator.portainer, self.device_id, ""
|
||||
)
|
||||
|
||||
|
||||
class PortainerContainerButton(PortainerContainerEntity, PortainerBaseButton):
|
||||
"""Defines a Portainer button."""
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
@@ -109,29 +205,10 @@ class PortainerButton(PortainerContainerEntity, ButtonEntity):
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger the Portainer button press service."""
|
||||
try:
|
||||
await self.entity_description.press_action(
|
||||
self.coordinator.portainer,
|
||||
self.endpoint_id,
|
||||
self.container_data.container.id,
|
||||
)
|
||||
except PortainerConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except PortainerAuthenticationError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except PortainerTimeoutError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Call the container button press action."""
|
||||
await self.entity_description.press_action(
|
||||
self.coordinator.portainer,
|
||||
self.endpoint_id,
|
||||
self.container_data.container.id,
|
||||
)
|
||||
|
||||
@@ -61,6 +61,14 @@
|
||||
"name": "Status"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"images_prune": {
|
||||
"name": "Prune unused images"
|
||||
},
|
||||
"restart_container": {
|
||||
"name": "Restart container"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"api_version": {
|
||||
"name": "API version"
|
||||
@@ -138,11 +146,20 @@
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while trying to connect to the Portainer instance: {error}"
|
||||
},
|
||||
"cannot_connect_no_details": {
|
||||
"message": "An error occurred while trying to connect to the Portainer instance."
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "An error occurred while trying to authenticate: {error}"
|
||||
},
|
||||
"invalid_auth_no_details": {
|
||||
"message": "An error occurred while trying to authenticate."
|
||||
},
|
||||
"timeout_connect": {
|
||||
"message": "A timeout occurred while trying to connect to the Portainer instance: {error}"
|
||||
},
|
||||
"timeout_connect_no_details": {
|
||||
"message": "A timeout occurred while trying to connect to the Portainer instance."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
PowerfoxConfigEntry,
|
||||
PowerfoxDataUpdateCoordinator,
|
||||
PowerfoxReportDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
@@ -30,12 +34,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) ->
|
||||
await client.close()
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
coordinators: list[PowerfoxDataUpdateCoordinator] = [
|
||||
PowerfoxDataUpdateCoordinator(hass, entry, client, device)
|
||||
for device in devices
|
||||
# Filter out gas meter devices (Powerfox FLOW adapters) as they are not yet supported and cause integration failures
|
||||
if device.type != DeviceType.GAS_METER
|
||||
]
|
||||
coordinators: list[
|
||||
PowerfoxDataUpdateCoordinator | PowerfoxReportDataUpdateCoordinator
|
||||
] = []
|
||||
for device in devices:
|
||||
if device.type == DeviceType.GAS_METER:
|
||||
coordinators.append(
|
||||
PowerfoxReportDataUpdateCoordinator(hass, entry, client, device)
|
||||
)
|
||||
continue
|
||||
coordinators.append(PowerfoxDataUpdateCoordinator(hass, entry, client, device))
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from powerfox import (
|
||||
Device,
|
||||
DeviceReport,
|
||||
Powerfox,
|
||||
PowerfoxAuthenticationError,
|
||||
PowerfoxConnectionError,
|
||||
@@ -15,14 +18,18 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
|
||||
type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxDataUpdateCoordinator]]
|
||||
type PowerfoxCoordinator = (
|
||||
"PowerfoxDataUpdateCoordinator" | "PowerfoxReportDataUpdateCoordinator"
|
||||
)
|
||||
type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxCoordinator]]
|
||||
|
||||
|
||||
class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
|
||||
"""Class to manage fetching Powerfox data from the API."""
|
||||
class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
|
||||
"""Base coordinator handling shared Powerfox logic."""
|
||||
|
||||
config_entry: PowerfoxConfigEntry
|
||||
|
||||
@@ -33,7 +40,7 @@ class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
|
||||
client: Powerfox,
|
||||
device: Device,
|
||||
) -> None:
|
||||
"""Initialize global Powerfox data updater."""
|
||||
"""Initialize shared Powerfox coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
@@ -44,11 +51,37 @@ class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
|
||||
self.client = client
|
||||
self.device = device
|
||||
|
||||
async def _async_update_data(self) -> Poweropti:
|
||||
"""Fetch data from Powerfox API."""
|
||||
async def _async_update_data(self) -> T:
|
||||
"""Fetch data and normalize Powerfox errors."""
|
||||
try:
|
||||
return await self.client.device(device_id=self.device.id)
|
||||
return await self._async_fetch_data()
|
||||
except PowerfoxAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
except (PowerfoxConnectionError, PowerfoxNoDataError) as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
async def _async_fetch_data(self) -> T:
|
||||
"""Fetch data from the Powerfox API."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PowerfoxDataUpdateCoordinator(PowerfoxBaseCoordinator[Poweropti]):
|
||||
"""Class to manage fetching Powerfox data from the API."""
|
||||
|
||||
async def _async_fetch_data(self) -> Poweropti:
|
||||
"""Fetch live device data from the Powerfox API."""
|
||||
return await self.client.device(device_id=self.device.id)
|
||||
|
||||
|
||||
class PowerfoxReportDataUpdateCoordinator(PowerfoxBaseCoordinator[DeviceReport]):
|
||||
"""Coordinator handling report data from the API."""
|
||||
|
||||
async def _async_fetch_data(self) -> DeviceReport:
|
||||
"""Fetch report data from the Powerfox API."""
|
||||
local_now = datetime.now(tz=dt_util.get_time_zone(self.hass.config.time_zone))
|
||||
return await self.client.report(
|
||||
device_id=self.device.id,
|
||||
year=local_now.year,
|
||||
month=local_now.month,
|
||||
day=local_now.day,
|
||||
)
|
||||
|
||||
@@ -5,18 +5,18 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from powerfox import HeatMeter, PowerMeter, WaterMeter
|
||||
from powerfox import DeviceReport, HeatMeter, PowerMeter, WaterMeter
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
|
||||
from .coordinator import PowerfoxConfigEntry
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: PowerfoxConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for Powerfox config entry."""
|
||||
powerfox_data: list[PowerfoxDataUpdateCoordinator] = entry.runtime_data
|
||||
powerfox_data = entry.runtime_data
|
||||
|
||||
return {
|
||||
"devices": [
|
||||
@@ -68,6 +68,21 @@ async def async_get_config_entry_diagnostics(
|
||||
if isinstance(coordinator.data, HeatMeter)
|
||||
else {}
|
||||
),
|
||||
**(
|
||||
{
|
||||
"gas_meter": {
|
||||
"sum": coordinator.data.gas.sum,
|
||||
"consumption": coordinator.data.gas.consumption,
|
||||
"consumption_kwh": coordinator.data.gas.consumption_kwh,
|
||||
"current_consumption": coordinator.data.gas.current_consumption,
|
||||
"current_consumption_kwh": coordinator.data.gas.current_consumption_kwh,
|
||||
"sum_currency": coordinator.data.gas.sum_currency,
|
||||
}
|
||||
}
|
||||
if isinstance(coordinator.data, DeviceReport)
|
||||
and coordinator.data.gas
|
||||
else {}
|
||||
),
|
||||
}
|
||||
for coordinator in powerfox_data
|
||||
],
|
||||
|
||||
@@ -2,23 +2,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from powerfox import Device
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PowerfoxDataUpdateCoordinator
|
||||
from .coordinator import PowerfoxBaseCoordinator
|
||||
|
||||
|
||||
class PowerfoxEntity(CoordinatorEntity[PowerfoxDataUpdateCoordinator]):
|
||||
class PowerfoxEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]](
|
||||
CoordinatorEntity[CoordinatorT]
|
||||
):
|
||||
"""Base entity for Powerfox."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PowerfoxDataUpdateCoordinator,
|
||||
coordinator: CoordinatorT,
|
||||
device: Device,
|
||||
) -> None:
|
||||
"""Initialize Powerfox entity."""
|
||||
|
||||
@@ -70,10 +70,7 @@ rules:
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not have any entities that should disabled by default.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
|
||||
@@ -4,8 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from powerfox import Device, HeatMeter, PowerMeter, WaterMeter
|
||||
from powerfox import Device, GasReport, HeatMeter, PowerMeter, WaterMeter
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -13,11 +14,16 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfVolume
|
||||
from homeassistant.const import CURRENCY_EURO, UnitOfEnergy, UnitOfPower, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
PowerfoxBaseCoordinator,
|
||||
PowerfoxConfigEntry,
|
||||
PowerfoxDataUpdateCoordinator,
|
||||
PowerfoxReportDataUpdateCoordinator,
|
||||
)
|
||||
from .entity import PowerfoxEntity
|
||||
|
||||
|
||||
@@ -30,6 +36,13 @@ class PowerfoxSensorEntityDescription[T: (PowerMeter, WaterMeter, HeatMeter)](
|
||||
value_fn: Callable[[T], float | int | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PowerfoxReportSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Powerfox report sensor entity."""
|
||||
|
||||
value_fn: Callable[[GasReport], float | int | None]
|
||||
|
||||
|
||||
SENSORS_POWER: tuple[PowerfoxSensorEntityDescription[PowerMeter], ...] = (
|
||||
PowerfoxSensorEntityDescription[PowerMeter](
|
||||
key="power",
|
||||
@@ -126,6 +139,104 @@ SENSORS_HEAT: tuple[PowerfoxSensorEntityDescription[HeatMeter], ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
SENSORS_GAS: tuple[PowerfoxReportSensorEntityDescription, ...] = (
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_consumption_today",
|
||||
translation_key="gas_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda gas: gas.sum,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_consumption_energy_today",
|
||||
translation_key="gas_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_current_consumption",
|
||||
translation_key="gas_current_consumption",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
value_fn=lambda gas: gas.current_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_current_consumption_energy",
|
||||
translation_key="gas_current_consumption_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.current_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_cost_today",
|
||||
translation_key="gas_cost_today",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
value_fn=lambda gas: gas.sum_currency,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_max_consumption_today",
|
||||
translation_key="gas_max_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
value_fn=lambda gas: gas.max_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_min_consumption_today",
|
||||
translation_key="gas_min_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
value_fn=lambda gas: gas.min_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_avg_consumption_today",
|
||||
translation_key="gas_avg_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.avg_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_max_consumption_energy_today",
|
||||
translation_key="gas_max_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.max_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_min_consumption_energy_today",
|
||||
translation_key="gas_min_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.min_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_avg_consumption_energy_today",
|
||||
translation_key="gas_avg_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.avg_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_max_cost_today",
|
||||
translation_key="gas_max_cost_today",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda gas: gas.max_currency,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -135,6 +246,20 @@ async def async_setup_entry(
|
||||
"""Set up Powerfox sensors based on a config entry."""
|
||||
entities: list[SensorEntity] = []
|
||||
for coordinator in entry.runtime_data:
|
||||
if isinstance(coordinator, PowerfoxReportDataUpdateCoordinator):
|
||||
gas_report = coordinator.data.gas
|
||||
if gas_report is None:
|
||||
continue
|
||||
entities.extend(
|
||||
PowerfoxGasSensorEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
device=coordinator.device,
|
||||
)
|
||||
for description in SENSORS_GAS
|
||||
if description.value_fn(gas_report) is not None
|
||||
)
|
||||
continue
|
||||
if isinstance(coordinator.data, PowerMeter):
|
||||
entities.extend(
|
||||
PowerfoxSensorEntity(
|
||||
@@ -166,23 +291,49 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class PowerfoxSensorEntity(PowerfoxEntity, SensorEntity):
|
||||
"""Defines a powerfox power meter sensor."""
|
||||
class BasePowerfoxSensorEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]](
|
||||
PowerfoxEntity[CoordinatorT], SensorEntity
|
||||
):
|
||||
"""Common base for Powerfox sensor entities."""
|
||||
|
||||
entity_description: PowerfoxSensorEntityDescription
|
||||
entity_description: SensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PowerfoxDataUpdateCoordinator,
|
||||
coordinator: CoordinatorT,
|
||||
device: Device,
|
||||
description: PowerfoxSensorEntityDescription,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Powerfox power meter sensor."""
|
||||
"""Initialize the shared Powerfox sensor."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.id}_{description.key}"
|
||||
|
||||
|
||||
class PowerfoxSensorEntity(BasePowerfoxSensorEntity[PowerfoxDataUpdateCoordinator]):
|
||||
"""Defines a powerfox poweropti sensor."""
|
||||
|
||||
coordinator: PowerfoxDataUpdateCoordinator
|
||||
entity_description: PowerfoxSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int | None:
|
||||
"""Return the state of the entity."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
|
||||
class PowerfoxGasSensorEntity(
|
||||
BasePowerfoxSensorEntity[PowerfoxReportDataUpdateCoordinator]
|
||||
):
|
||||
"""Defines a powerfox gas meter sensor."""
|
||||
|
||||
coordinator: PowerfoxReportDataUpdateCoordinator
|
||||
entity_description: PowerfoxReportSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int | None:
|
||||
"""Return the state of the entity."""
|
||||
gas_report = self.coordinator.data.gas
|
||||
if TYPE_CHECKING:
|
||||
assert gas_report is not None
|
||||
return self.entity_description.value_fn(gas_report)
|
||||
|
||||
@@ -62,6 +62,42 @@
|
||||
"energy_usage_low_tariff": {
|
||||
"name": "Energy usage low tariff"
|
||||
},
|
||||
"gas_avg_consumption_energy_today": {
|
||||
"name": "Avg gas hourly energy - today"
|
||||
},
|
||||
"gas_avg_consumption_today": {
|
||||
"name": "Avg gas hourly consumption - today"
|
||||
},
|
||||
"gas_consumption_energy_today": {
|
||||
"name": "Gas consumption energy - today"
|
||||
},
|
||||
"gas_consumption_today": {
|
||||
"name": "Gas consumption - today"
|
||||
},
|
||||
"gas_cost_today": {
|
||||
"name": "Gas cost - today"
|
||||
},
|
||||
"gas_current_consumption": {
|
||||
"name": "Gas consumption - this hour"
|
||||
},
|
||||
"gas_current_consumption_energy": {
|
||||
"name": "Gas consumption energy - this hour"
|
||||
},
|
||||
"gas_max_consumption_energy_today": {
|
||||
"name": "Max gas hourly energy - today"
|
||||
},
|
||||
"gas_max_consumption_today": {
|
||||
"name": "Max gas hourly consumption - today"
|
||||
},
|
||||
"gas_max_cost_today": {
|
||||
"name": "Max gas hourly cost - today"
|
||||
},
|
||||
"gas_min_consumption_energy_today": {
|
||||
"name": "Min gas hourly energy - today"
|
||||
},
|
||||
"gas_min_consumption_today": {
|
||||
"name": "Min gas hourly consumption - today"
|
||||
},
|
||||
"heat_delta_energy": {
|
||||
"name": "Delta energy"
|
||||
},
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ruuvitag_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["ruuvitag-ble==0.3.0"]
|
||||
"requirements": ["ruuvitag-ble==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -151,6 +151,12 @@ SENSOR_DESCRIPTIONS = {
|
||||
translation_key="nox_index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"iaqs": SensorEntityDescription(
|
||||
key="iaqs",
|
||||
translation_key="iaqs",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
"acceleration_z": {
|
||||
"name": "Acceleration Z"
|
||||
},
|
||||
"iaqs": {
|
||||
"name": "Indoor air quality score"
|
||||
},
|
||||
"movement_counter": {
|
||||
"name": "Movement counter"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
@@ -25,11 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) ->
|
||||
"""Set up Saunum Leil Sauna from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
client = SaunumClient(host=host)
|
||||
|
||||
# Test connection
|
||||
try:
|
||||
await client.connect()
|
||||
client = await SaunumClient.create(host)
|
||||
except SaunumConnectionError as exc:
|
||||
raise ConfigEntryNotReady(f"Error connecting to {host}: {exc}") from exc
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user