Compare commits

..

10 Commits

Author SHA1 Message Date
Petar Petrov
5f4ffd6f8a Add availability checks 2026-01-08 18:07:00 +02:00
Petar Petrov
294c93e3ed PR comments 2026-01-08 17:53:20 +02:00
Petar Petrov
51faa35f1b Update homeassistant/components/energy/data.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 17:35:05 +02:00
Petar Petrov
303a4091a7 Handle different units in combined sensors 2026-01-08 15:41:08 +02:00
Petar Petrov
fc9a86b919 naming 2026-01-08 15:15:18 +02:00
Petar Petrov
2be7b57e48 validation tweak 2026-01-08 14:09:37 +02:00
Petar Petrov
27ecfd1319 type fix 2026-01-08 10:59:25 +02:00
Petar Petrov
ade50c93cf typing 2026-01-08 09:28:33 +02:00
Petar Petrov
b029a48ed4 add tests 2026-01-08 09:09:03 +02:00
Petar Petrov
b05a6dadf6 Add non standard power sensor support 2026-01-07 16:51:59 +02:00
360 changed files with 3254 additions and 13092 deletions

6
CODEOWNERS generated
View File

@@ -661,8 +661,6 @@ 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
@@ -1172,8 +1170,8 @@ build.json @home-assistant/supervisor
/tests/components/open_router/ @joostlek
/homeassistant/components/openerz/ @misialq
/tests/components/openerz/ @misialq
/homeassistant/components/openevse/ @c00w @firstof9
/tests/components/openevse/ @c00w @firstof9
/homeassistant/components/openevse/ @c00w
/tests/components/openevse/ @c00w
/homeassistant/components/openexchangerates/ @MartinHjelmare
/tests/components/openexchangerates/ @MartinHjelmare
/homeassistant/components/opengarage/ @danielhiversen

View File

@@ -43,13 +43,6 @@ 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(),
),
)

View File

@@ -1,10 +1,5 @@
{
"entity": {
"button": {
"recalibrate_co2": {
"default": "mdi:molecule-co2"
}
},
"number": {
"hysteresis_band": {
"default": "mdi:delta"

View File

@@ -12,6 +12,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyairobotrest"],
"quality_scale": "gold",
"quality_scale": "silver",
"requirements": ["pyairobotrest==0.2.0"]
}

View File

@@ -43,7 +43,7 @@ rules:
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done

View File

@@ -59,11 +59,6 @@
}
},
"entity": {
"button": {
"recalibrate_co2": {
"name": "Recalibrate CO2 sensor"
}
},
"number": {
"hysteresis_band": {
"name": "Hysteresis band"

View File

@@ -85,22 +85,6 @@ class AirzoneSystemEntity(AirzoneEntity):
value = system[key]
return value
async def _async_update_sys_params(self, params: dict[str, Any]) -> None:
"""Send system parameters to API."""
_params = {
API_SYSTEM_ID: self.system_id,
**params,
}
_LOGGER.debug("update_sys_params=%s", _params)
try:
await self.coordinator.airzone.set_sys_parameters(_params)
except AirzoneError as error:
raise HomeAssistantError(
f"Failed to set system {self.entity_id}: {error}"
) from error
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
class AirzoneHotWaterEntity(AirzoneEntity):
"""Define an Airzone Hot Water entity."""

View File

@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.5"]
"requirements": ["aioairzone==1.0.4"]
}

View File

@@ -20,7 +20,6 @@ from aioairzone.const import (
AZD_MODES,
AZD_Q_ADAPT,
AZD_SLEEP,
AZD_SYSTEMS,
AZD_ZONES,
)
@@ -31,7 +30,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
from .entity import AirzoneEntity, AirzoneZoneEntity
@dataclass(frozen=True, kw_only=True)
@@ -86,18 +85,6 @@ def main_zone_options(
return [k for k, v in options.items() if v in modes]
SYSTEM_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_Q_ADAPT,
entity_category=EntityCategory.CONFIG,
key=AZD_Q_ADAPT,
options=list(Q_ADAPT_DICT),
options_dict=Q_ADAPT_DICT,
translation_key="q_adapt",
),
)
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_MODE,
@@ -106,6 +93,14 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
options_fn=main_zone_options,
translation_key="modes",
),
AirzoneSelectDescription(
api_param=API_Q_ADAPT,
entity_category=EntityCategory.CONFIG,
key=AZD_Q_ADAPT,
options=list(Q_ADAPT_DICT),
options_dict=Q_ADAPT_DICT,
translation_key="q_adapt",
),
)
@@ -145,37 +140,16 @@ async def async_setup_entry(
"""Add Airzone select from a config_entry."""
coordinator = entry.runtime_data
added_systems: set[str] = set()
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of select."""
entities: list[AirzoneBaseSelect] = []
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
received_systems = set(systems_data)
new_systems = received_systems - added_systems
if new_systems:
entities.extend(
AirzoneSystemSelect(
coordinator,
description,
entry,
system_id,
systems_data.get(system_id),
)
for system_id in new_systems
for description in SYSTEM_SELECT_TYPES
if description.key in systems_data.get(system_id)
)
added_systems.update(new_systems)
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
entities: list[AirzoneZoneSelect] = [
AirzoneZoneSelect(
coordinator,
description,
@@ -187,8 +161,8 @@ async def async_setup_entry(
for description in MAIN_ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
)
entities.extend(
]
entities += [
AirzoneZoneSelect(
coordinator,
description,
@@ -199,11 +173,10 @@ async def async_setup_entry(
for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
)
]
async_add_entities(entities)
added_zones.update(new_zones)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
@@ -230,38 +203,6 @@ class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
self._attr_current_option = self._get_current_option()
class AirzoneSystemSelect(AirzoneSystemEntity, AirzoneBaseSelect):
"""Define an Airzone System select."""
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: AirzoneSelectDescription,
entry: ConfigEntry,
system_id: str,
system_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, entry, system_data)
self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}"
self.entity_description = description
self._attr_options = self.entity_description.options_fn(
system_data, description.options_dict
)
self.values_dict = {v: k for k, v in description.options_dict.items()}
self._async_update_attrs()
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
param = self.entity_description.api_param
value = self.entity_description.options_dict[option]
await self._async_update_sys_params({param: value})
class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
"""Define an Airzone Zone select."""

View File

@@ -69,7 +69,6 @@ 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
@@ -194,7 +193,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"] != (

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
import logging
import dateutil
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.sensor import (
@@ -181,7 +179,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
LAST_S_TEST: SensorEntityDescription(
key=LAST_S_TEST,
translation_key="last_self_test",
device_class=SensorDeviceClass.TIMESTAMP,
),
"lastxfer": SensorEntityDescription(
key="lastxfer",
@@ -235,7 +232,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
"masterupd": SensorEntityDescription(
key="masterupd",
translation_key="master_update",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"maxlinev": SensorEntityDescription(
@@ -369,7 +365,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
"starttime": SensorEntityDescription(
key="starttime",
translation_key="startup_time",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"statflag": SensorEntityDescription(
@@ -421,19 +416,16 @@ SENSORS: dict[str, SensorEntityDescription] = {
"xoffbat": SensorEntityDescription(
key="xoffbat",
translation_key="transfer_from_battery",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"xoffbatt": SensorEntityDescription(
key="xoffbatt",
translation_key="transfer_from_battery",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"xonbatt": SensorEntityDescription(
key="xonbatt",
translation_key="transfer_to_battery",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
}
@@ -537,13 +529,7 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
self._attr_native_value = None
return
data = self.coordinator.data[key]
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
self._attr_native_value = dateutil.parser.parse(data)
return
self._attr_native_value, inferred_unit = infer_unit(data)
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
if not self.native_unit_of_measurement:
self._attr_native_unit_of_measurement = inferred_unit

View File

@@ -8,5 +8,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["pysilero-vad==3.2.0", "pyspeex-noise==1.0.2"]
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
}

View File

@@ -36,10 +36,6 @@ _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."""
@@ -333,28 +329,13 @@ class BackblazeBackupAgent(BackupAgent):
_LOGGER.debug("Uploading backup file %s with streaming", filename)
try:
content_type, _ = mimetypes.guess_type(filename)
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,
file_version = await self._hass.async_add_executor_job(
self._upload_unbound_stream_sync,
reader,
filename,
content_type or "application/x-tar",
file_info,
)
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()

View File

@@ -34,12 +34,7 @@ class BeoData:
type BeoConfigEntry = ConfigEntry[BeoData]
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.EVENT,
Platform.MEDIA_PLAYER,
Platform.SENSOR,
]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:

View File

@@ -1,63 +0,0 @@
"""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()

View File

@@ -115,7 +115,6 @@ 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"

View File

@@ -4,10 +4,8 @@ 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
@@ -57,19 +55,6 @@ 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(
@@ -87,26 +72,4 @@ 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

View File

@@ -1,139 +0,0 @@
"""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

View File

@@ -84,10 +84,3 @@ 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"

View File

@@ -6,7 +6,6 @@ import logging
from typing import TYPE_CHECKING
from mozart_api.models import (
BatteryState,
BeoRemoteButton,
ButtonEvent,
ListeningModeProps,
@@ -61,7 +60,6 @@ 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
)
@@ -117,14 +115,6 @@ 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:

View File

@@ -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, HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -56,31 +56,8 @@ 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:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error",
translation_placeholders={
"device": self.config_entry.title,
"error": repr(err),
},
) from err
_LOGGER.error("Command error: %s", err)
await self.async_request_refresh()
return wrapper
@@ -188,35 +165,17 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
if self.skipped_updates < 10:
self.connected = False
self.skipped_updates += 1
_LOGGER.debug(
"Update for %s skipped: the Bravia API service is reloading",
self.config_entry.title,
)
_LOGGER.debug("Update skipped, Bravia API service is reloading")
return
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error_not_found",
translation_placeholders={
"device": self.config_entry.title,
},
) from err
raise UpdateFailed("Error communicating with device") from err
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff):
self.is_on = False
self.connected = False
_LOGGER.debug(
"Update for %s skipped: the TV is turned off", self.config_entry.title
)
_LOGGER.debug("Update skipped, Bravia TV is off")
except BraviaError as err:
self.is_on = False
self.connected = False
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={
"device": self.config_entry.title,
"error": repr(err),
},
) from err
raise UpdateFailed("Error communicating with device") from err
async def async_update_volume(self) -> None:
"""Update volume information."""

View File

@@ -55,22 +55,5 @@
"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"
}
}
}

View File

@@ -111,17 +111,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
return None
return self.coordinator.data.state.target_temperature.value
@property
def _hvac_mode_value(self) -> int | str | None:
"""Return the raw hvac_mode value from the coordinator."""
if (hvac_mode := self.coordinator.data.state.hvac_mode) is None:
return None
return hvac_mode.value
@property
def hvac_mode(self) -> HVACMode | None:
"""Return hvac operation ie. heat, cool mode."""
if (hvac_mode_value := self._hvac_mode_value) is None:
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
if hvac_mode_value is None:
return None
# BSB-Lan returns integer values: 0=off, 1=auto, 2=eco, 3=heat
if isinstance(hvac_mode_value, int):
@@ -131,8 +125,9 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
# BSB-Lan mode 2 is eco/reduced mode
if self._hvac_mode_value == 2:
if hvac_mode_value == 2:
return PRESET_ECO
return PRESET_NONE

View File

@@ -29,11 +29,7 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
name=data.device.name,
manufacturer="BSBLAN Inc.",
model=(
data.info.device_identification.value
if data.info.device_identification
else None
),
model=data.info.device_identification.value,
sw_version=data.device.version,
configuration_url=f"http://{host}",
)

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==4.1.0"],
"requirements": ["python-bsblan==3.1.6"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.16.0"]
"requirements": ["bthome-ble==3.17.0"]
}

View File

@@ -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), [vol.Coerce(HVACMode)]
cv.ensure_list, vol.Length(min=1), [HVACMode]
),
},
}

View File

@@ -19,10 +19,6 @@
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
@@ -31,11 +27,14 @@
- 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:

View File

@@ -50,6 +50,7 @@ from . import (
from .client import CloudClient
from .const import (
CONF_ACCOUNT_LINK_SERVER,
CONF_ACCOUNTS_SERVER,
CONF_ACME_SERVER,
CONF_ALEXA,
CONF_ALIASES,
@@ -137,6 +138,7 @@ _BASE_CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
vol.Optional(CONF_ACCOUNTS_SERVER): str,
vol.Optional(CONF_ACME_SERVER): str,
vol.Optional(CONF_API_SERVER): str,
vol.Optional(CONF_RELAYER_SERVER): str,

View File

@@ -76,6 +76,7 @@ CONF_GOOGLE_ACTIONS = "google_actions"
CONF_USER_POOL_ID = "user_pool_id"
CONF_ACCOUNT_LINK_SERVER = "account_link_server"
CONF_ACCOUNTS_SERVER = "accounts_server"
CONF_ACME_SERVER = "acme_server"
CONF_API_SERVER = "api_server"
CONF_DISCOVERY_SERVICE_ACTIONS = "discovery_service_actions"

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.9.0"],
"requirements": ["hass-nabucasa==1.7.0"],
"single_config_entry": true
}

View File

@@ -169,7 +169,6 @@ 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},
@@ -625,7 +624,6 @@ 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,

View File

@@ -28,11 +28,10 @@ async def async_setup_entry(
DemoHumidifier(
name="Humidifier",
mode=None,
target_humidity=65,
target_humidity=68,
current_humidity=45,
action=HumidifierAction.HUMIDIFYING,
device_class=HumidifierDeviceClass.HUMIDIFIER,
target_humidity_step=5,
),
DemoHumidifier(
name="Dehumidifier",
@@ -67,7 +66,6 @@ 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
@@ -81,7 +79,6 @@ 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."""

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==4.0.0"]
"requirements": ["aiodns==3.6.1"]
}

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["easyenergy==2.2.0"],
"requirements": ["easyenergy==2.1.2"],
"single_config_entry": true
}

View File

@@ -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.1"]
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
}

View File

@@ -37,7 +37,7 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
name=device.name,
connections={(CONNECTION_NETWORK_MAC, device.mac_address)},
manufacturer="EHEIM",
model=device.model_name,
model=device.device_type.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 eheimdigital calls to handle exceptions.
"""Decorate AirGradient calls to handle exceptions.
A decorator that wraps the passed in function, catches eheimdigital errors.
A decorator that wraps the passed in function, catches AirGradient errors.
"""
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:

View File

@@ -6,7 +6,6 @@ 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
@@ -22,7 +21,6 @@ from homeassistant.const import (
PRECISION_WHOLE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -44,34 +42,6 @@ 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], ...
] = (
@@ -175,13 +145,6 @@ 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](

View File

@@ -2,19 +2,13 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, Literal, override
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 (
FilterMode,
FilterModeProf,
UnitOfMeasurement as EheimDigitalUnitOfMeasurement,
)
from eheimdigital.types import FilterMode
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
@@ -30,109 +24,8 @@ 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] | 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))
)
),
),
)
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
CLASSICVARIO_DESCRIPTIONS: tuple[
@@ -141,7 +34,11 @@ CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSelectDescription[EheimDigitalClassicVario](
key="filter_mode",
translation_key="filter_mode",
value_fn=lambda device: device.filter_mode.name.lower(),
value_fn=(
lambda device: device.filter_mode.name.lower()
if device.filter_mode is not None
else None
),
set_value_fn=(
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
),
@@ -171,11 +68,6 @@ 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)
@@ -190,8 +82,6 @@ class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
entity_description: EheimDigitalSelectDescription[_DeviceT]
_attr_options: list[str]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
@@ -201,49 +91,13 @@ 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:
if await_return := self.entity_description.set_value_fn(self._device, option):
return await await_return
return None
return await self.entity_description.set_value_fn(self._device, option)
@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()

View File

@@ -6,7 +6,6 @@ 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 (
@@ -14,7 +13,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfFrequency, UnitOfTime
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -34,27 +33,6 @@ 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], ...
] = (
@@ -76,7 +54,11 @@ CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSensorDescription[EheimDigitalClassicVario](
key="error_code",
translation_key="error_code",
value_fn=lambda device: device.error_code.name.lower(),
value_fn=(
lambda device: device.error_code.name.lower()
if device.error_code is not None
else None
),
device_class=SensorDeviceClass.ENUM,
options=[name.lower() for name in FilterErrorCode._member_names_],
entity_category=EntityCategory.DIAGNOSTIC,
@@ -98,13 +80,6 @@ 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](

View File

@@ -61,12 +61,6 @@
"day_speed": {
"name": "Day speed"
},
"high_pulse_time": {
"name": "High pulse duration"
},
"low_pulse_time": {
"name": "Low pulse duration"
},
"manual_speed": {
"name": "Manual speed"
},
@@ -84,32 +78,13 @@
}
},
"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": {
@@ -124,17 +99,8 @@
"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": {

View File

@@ -4,7 +4,6 @@ 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
@@ -31,8 +30,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, EheimDigitalFilter)):
entities.append(EheimDigitalFilterSwitch(coordinator, device)) # noqa: PERF401
if isinstance(device, EheimDigitalClassicVario):
entities.append(EheimDigitalClassicVarioSwitch(coordinator, device)) # noqa: PERF401
async_add_entities(entities)
@@ -40,10 +39,10 @@ async def async_setup_entry(
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalFilterSwitch(
EheimDigitalEntity[EheimDigitalClassicVario | EheimDigitalFilter], SwitchEntity
class EheimDigitalClassicVarioSwitch(
EheimDigitalEntity[EheimDigitalClassicVario], SwitchEntity
):
"""Represent an EHEIM Digital classicVARIO or filter switch entity."""
"""Represent an EHEIM Digital classicVARIO switch entity."""
_attr_translation_key = "filter_active"
_attr_name = None
@@ -51,9 +50,9 @@ class EheimDigitalFilterSwitch(
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: EheimDigitalClassicVario | EheimDigitalFilter,
device: EheimDigitalClassicVario,
) -> None:
"""Initialize an EHEIM Digital classicVARIO or filter switch entity."""
"""Initialize an EHEIM Digital classicVARIO switch entity."""
super().__init__(coordinator, device)
self._attr_unique_id = device.mac_address
self._async_update_attrs()

View File

@@ -7,7 +7,6 @@ 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
@@ -29,23 +28,6 @@ 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], ...
] = (
@@ -97,13 +79,6 @@ 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](

View File

@@ -59,13 +59,38 @@ class FlowToGridSourceType(TypedDict):
number_energy_price: float | None # Price for energy ($/kWh)
class GridPowerSourceType(TypedDict):
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):
"""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."""
@@ -97,8 +122,12 @@ 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."""
@@ -211,10 +240,47 @@ FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema(
}
)
GRID_POWER_SOURCE_SCHEMA = vol.Schema(
{
vol.Required("stat_rate"): str,
}
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,
)
@@ -225,7 +291,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(flow_from[key] for flow_from in val)
counts = Counter(item.get(key) for item in val if item.get(key) is not None)
for value, count in counts.items():
if count > 1:
@@ -267,7 +333,10 @@ 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(
@@ -387,6 +456,12 @@ 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)
@@ -395,6 +470,74 @@ 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."""

View File

@@ -0,0 +1,35 @@
"""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 ""

View File

@@ -19,7 +19,12 @@ 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, UnitOfVolume
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
UnitOfEnergy,
UnitOfPower,
UnitOfVolume,
)
from homeassistant.core import (
HomeAssistant,
State,
@@ -36,7 +41,8 @@ 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, async_get_manager
from .data import EnergyManager, PowerConfig, async_get_manager
from .helpers import generate_power_sensor_entity_id, generate_power_sensor_unique_id
SUPPORTED_STATE_CLASSES = {
SensorStateClass.MEASUREMENT,
@@ -137,6 +143,7 @@ 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."""
@@ -147,8 +154,9 @@ class SensorManager:
async def _process_manager_data(self) -> None:
"""Process manager data."""
to_add: list[EnergyCostSensor] = []
to_add: list[EnergyCostSensor | EnergyPowerSensor] = []
to_remove = dict(self.current_entities)
power_to_remove = dict(self.current_power_entities)
async def finish() -> None:
if to_add:
@@ -159,6 +167,10 @@ 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
@@ -185,6 +197,13 @@ 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
@@ -192,7 +211,7 @@ class SensorManager:
self,
adapter: SourceAdapter,
config: Mapping[str, Any],
to_add: list[EnergyCostSensor],
to_add: list[EnergyCostSensor | EnergyPowerSensor],
to_remove: dict[tuple[str, str | None, str], EnergyCostSensor],
) -> None:
"""Process sensor data."""
@@ -220,6 +239,74 @@ 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."""
@@ -495,3 +582,196 @@ 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()

View File

@@ -206,7 +206,7 @@ class EnvoyProductionSensorEntityDescription(SensorEntityDescription):
"""Describes an Envoy production sensor entity."""
value_fn: Callable[[EnvoySystemProduction], int]
on_phase: str | None = None
on_phase: str | None
PRODUCTION_SENSORS = (
@@ -219,6 +219,7 @@ 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",
@@ -229,6 +230,7 @@ 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",
@@ -238,6 +240,7 @@ 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",
@@ -248,6 +251,7 @@ PRODUCTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("watt_hours_lifetime"),
on_phase=None,
),
)
@@ -273,7 +277,7 @@ class EnvoyConsumptionSensorEntityDescription(SensorEntityDescription):
"""Describes an Envoy consumption sensor entity."""
value_fn: Callable[[EnvoySystemConsumption], int]
on_phase: str | None = None
on_phase: str | None
CONSUMPTION_SENSORS = (
@@ -286,6 +290,7 @@ 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",
@@ -296,6 +301,7 @@ 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",
@@ -305,6 +311,7 @@ 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",
@@ -315,6 +322,7 @@ CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("watt_hours_lifetime"),
on_phase=None,
),
)
@@ -346,6 +354,7 @@ 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",
@@ -357,6 +366,7 @@ NET_CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("watt_hours_lifetime"),
on_phase=None,
),
)
@@ -385,7 +395,7 @@ class EnvoyCTSensorEntityDescription(SensorEntityDescription):
[EnvoyMeterData],
int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None,
]
on_phase: str | None = None
on_phase: str | None
cttype: str | None = None
@@ -401,6 +411,7 @@ 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 (
@@ -419,6 +430,7 @@ 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 (
@@ -437,6 +449,7 @@ 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 (
@@ -455,6 +468,7 @@ 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 (
@@ -474,6 +488,7 @@ 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 (
@@ -493,6 +508,7 @@ CT_SENSORS = (
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("current"),
on_phase=None,
cttype=cttype,
)
for cttype, key in (
@@ -510,6 +526,7 @@ 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 (
@@ -527,6 +544,7 @@ 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 (
@@ -547,6 +565,7 @@ 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 (
@@ -764,7 +783,7 @@ ENCHARGE_AGGREGATE_SENSORS = (
translation_key="available_energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.ENERGY_STORAGE,
device_class=SensorDeviceClass.ENERGY,
value_fn=attrgetter("available_energy"),
),
EnvoyEnchargeAggregateSensorEntityDescription(
@@ -772,14 +791,14 @@ ENCHARGE_AGGREGATE_SENSORS = (
translation_key="reserve_energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.ENERGY_STORAGE,
device_class=SensorDeviceClass.ENERGY,
value_fn=attrgetter("backup_reserve"),
),
EnvoyEnchargeAggregateSensorEntityDescription(
key="max_capacity",
translation_key="max_capacity",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY_STORAGE,
device_class=SensorDeviceClass.ENERGY,
value_fn=attrgetter("max_available_capacity"),
),
)

View File

@@ -461,7 +461,7 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
key="sleep/timeInBed",
translation_key="sleep_time_in_bed",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:bed",
icon="mdi:hotel",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.SLEEP,
state_class=SensorStateClass.TOTAL_INCREASING,

View File

@@ -164,12 +164,13 @@ def _async_wol_buttons_list(
class FritzBoxWOLButton(FritzDeviceBase, ButtonEntity):
"""Defines a FRITZ!Box Tools Wake On LAN button."""
_attr_icon = "mdi:lan-pending"
_attr_entity_registry_enabled_default = False
_attr_translation_key = "wake_on_lan"
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
"""Initialize Fritz!Box WOL button."""
super().__init__(avm_wrapper, device)
self._name = f"{self.hostname} Wake on LAN"
self._attr_unique_id = f"{self._mac}_wake_on_lan"
self._is_available = True

View File

@@ -10,7 +10,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DEFAULT_DEVICE_NAME
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
from .entity import FritzDeviceBase
from .helpers import device_filter_out_from_trackers
@@ -72,7 +71,6 @@ class FritzBoxTracker(FritzDeviceBase, ScannerEntity):
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
"""Initialize a FRITZ!Box device."""
super().__init__(avm_wrapper, device)
self._attr_name: str = device.hostname or DEFAULT_DEVICE_NAME
self._last_activity: datetime.datetime | None = device.last_activity
@property

View File

@@ -13,7 +13,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .const import DEFAULT_DEVICE_NAME, DOMAIN
from .coordinator import AvmWrapper
from .models import FritzDevice
@@ -21,17 +21,21 @@ from .models import FritzDevice
class FritzDeviceBase(CoordinatorEntity[AvmWrapper]):
"""Entity base class for a device connected to a FRITZ!Box device."""
_attr_has_entity_name = True
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
"""Initialize a FRITZ!Box device."""
super().__init__(avm_wrapper)
self._avm_wrapper = avm_wrapper
self._mac: str = device.mac_address
self._name: str = device.hostname or DEFAULT_DEVICE_NAME
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, device.mac_address)}
)
@property
def name(self) -> str:
"""Return device name."""
return self._name
@property
def ip_address(self) -> str | None:
"""Return the primary ip address of the device."""

View File

@@ -3,9 +3,6 @@
"button": {
"cleanup": {
"default": "mdi:broom"
},
"wake_on_lan": {
"default": "mdi:lan-pending"
}
},
"sensor": {
@@ -51,11 +48,6 @@
"max_kb_s_sent": {
"default": "mdi:upload"
}
},
"switch": {
"internet_access": {
"default": "mdi:router-wireless-settings"
}
}
},
"services": {

View File

@@ -8,7 +8,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"quality_scale": "bronze",
"requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==1.0.2"],
"ssdp": [
{

View File

@@ -13,7 +13,9 @@ rules:
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
has-entity-name:
status: todo
comment: partially done
runtime-data: done
test-before-configure: done
test-before-setup: done

View File

@@ -108,9 +108,6 @@
},
"reconnect": {
"name": "Reconnect"
},
"wake_on_lan": {
"name": "Wake on LAN"
}
},
"sensor": {
@@ -165,11 +162,6 @@
"max_kb_s_sent": {
"name": "Max connection upload throughput"
}
},
"switch": {
"internet_access": {
"name": "Internet access"
}
}
},
"exceptions": {

View File

@@ -499,12 +499,13 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
"""Defines a FRITZ!Box Tools DeviceProfile switch."""
_attr_translation_key = "internet_access"
_attr_icon = "mdi:router-wireless-settings"
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
"""Init Fritz profile."""
super().__init__(avm_wrapper, device)
self._attr_is_on: bool = False
self._name = f"{device.hostname} Internet Access"
self._attr_unique_id = f"{self._mac}_internet_access"
self._attr_entity_category = EntityCategory.CONFIG

View File

@@ -77,14 +77,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
)
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
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
self.has_triggers = await self.hass.async_add_executor_job(
self.fritz.has_triggers
)
LOGGER.debug("enable smarthome triggers: %s", self.has_triggers)
self.configuration_url = self.fritz.get_prefixed_host()

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260107.1"]
"requirements": ["home-assistant-frontend==20251229.1"]
}

View File

@@ -1,21 +1,9 @@
{
"entity": {
"sensor": {
"ammonia": {
"default": "mdi:molecule"
},
"benzene": {
"default": "mdi:molecule"
},
"nitrogen_dioxide": {
"default": "mdi:molecule"
},
"nitrogen_monoxide": {
"default": "mdi:molecule"
},
"non_methane_hydrocarbons": {
"default": "mdi:molecule"
},
"ozone": {
"default": "mdi:molecule"
},

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==2.1.2"]
"requirements": ["google_air_quality_api==2.0.2"]
}

View File

@@ -99,14 +99,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
"local_aqi": data.indexes[1].display_name
},
),
AirQualitySensorEntityDescription(
key="c6h6",
translation_key="benzene",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.c6h6.concentration.units,
value_fn=lambda x: x.pollutants.c6h6.concentration.value,
exists_fn=lambda x: "c6h6" in {p.code for p in x.pollutants},
),
AirQualitySensorEntityDescription(
key="co",
state_class=SensorStateClass.MEASUREMENT,
@@ -114,30 +106,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
native_unit_of_measurement_fn=lambda x: x.pollutants.co.concentration.units,
value_fn=lambda x: x.pollutants.co.concentration.value,
),
AirQualitySensorEntityDescription(
key="nh3",
translation_key="ammonia",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.nh3.concentration.units,
value_fn=lambda x: x.pollutants.nh3.concentration.value,
exists_fn=lambda x: "nh3" in {p.code for p in x.pollutants},
),
AirQualitySensorEntityDescription(
key="nmhc",
translation_key="non_methane_hydrocarbons",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.nmhc.concentration.units,
value_fn=lambda x: x.pollutants.nmhc.concentration.value,
exists_fn=lambda x: "nmhc" in {p.code for p in x.pollutants},
),
AirQualitySensorEntityDescription(
key="no",
translation_key="nitrogen_monoxide",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.no.concentration.units,
value_fn=lambda x: x.pollutants.no.concentration.value,
exists_fn=lambda x: "no" in {p.code for p in x.pollutants},
),
AirQualitySensorEntityDescription(
key="no2",
translation_key="nitrogen_dioxide",

View File

@@ -76,12 +76,6 @@
},
"entity": {
"sensor": {
"ammonia": {
"name": "Ammonia"
},
"benzene": {
"name": "Benzene"
},
"local_aqi": {
"name": "{local_aqi} AQI"
},
@@ -195,9 +189,6 @@
"name": "{local_aqi} dominant pollutant",
"state": {
"co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"nh3": "[%key:component::google_air_quality::entity::sensor::ammonia::name%]",
"nmhc": "[%key:component::google_air_quality::entity::sensor::non_methane_hydrocarbons::name%]",
"no": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
"no2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
"o3": "[%key:component::sensor::entity_component::ozone::name%]",
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
@@ -208,12 +199,6 @@
"nitrogen_dioxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
},
"nitrogen_monoxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]"
},
"non_methane_hydrocarbons": {
"name": "Non-methane hydrocarbons"
},
"ozone": {
"name": "[%key:component::sensor::entity_component::ozone::name%]"
},

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/gree",
"iot_class": "local_polling",
"loggers": ["greeclimate"],
"requirements": ["greeclimate==2.1.1"]
"requirements": ["greeclimate==2.1.0"]
}

View File

@@ -1,31 +0,0 @@
"""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)

View File

@@ -1,74 +0,0 @@
"""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

View File

@@ -1,54 +0,0 @@
"""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"]

View File

@@ -1,3 +0,0 @@
"""Constants for HDFury Integration."""
DOMAIN = "hdfury"

View File

@@ -1,67 +0,0 @@
"""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,
)

View File

@@ -1,21 +0,0 @@
"""Diagnostics for HDFury Integration."""
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .coordinator import HDFuryCoordinator
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: HDFuryCoordinator = entry.runtime_data
return {
"board": coordinator.data.board,
"info": coordinator.data.info,
"config": coordinator.data.config,
}

View File

@@ -1,39 +0,0 @@
"""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"])
},
)

View File

@@ -1,52 +0,0 @@
{
"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"
}
}
}
}

View File

@@ -1,11 +0,0 @@
{
"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"]
}

View File

@@ -1,74 +0,0 @@
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: done
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

View File

@@ -1,122 +0,0 @@
"""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()

View File

@@ -1,101 +0,0 @@
{
"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}"
}
}
}

View File

@@ -1,142 +0,0 @@
"""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()

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from dataclasses import dataclass
import logging
from pyhik.constants import SENSOR_MAP
from pyhik.hikvision import HikCamera
import requests
@@ -71,33 +70,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
device_type=device_type,
)
_LOGGER.debug(
"Device %s (type=%s) initial event_states: %s",
device_name,
device_type,
camera.current_event_states,
)
# For NVRs or devices with no detected events, try to fetch events from ISAPI
# Use broader notification methods for NVRs since they often use 'record' etc.
if device_type == "NVR" or not camera.current_event_states:
nvr_notification_methods = {"center", "HTTP", "record", "email", "beep"}
def fetch_and_inject_nvr_events() -> None:
"""Fetch and inject NVR events in a single executor job."""
nvr_events = camera.get_event_triggers(nvr_notification_methods)
_LOGGER.debug("NVR events fetched with extended methods: %s", nvr_events)
if nvr_events:
# Map raw event type names to friendly names using SENSOR_MAP
mapped_events: dict[str, list[int]] = {}
for event_type, channels in nvr_events.items():
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
if friendly_name in mapped_events:
mapped_events[friendly_name].extend(channels)
else:
mapped_events[friendly_name] = list(channels)
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
camera.inject_events(mapped_events)
if nvr_events := camera.get_event_triggers():
camera.inject_events(nvr_events)
await hass.async_add_executor_job(fetch_and_inject_nvr_events)

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["pyhik"],
"quality_scale": "legacy",
"requirements": ["pyHik==0.4.0"]
"requirements": ["pyHik==0.3.4"]
}

View File

@@ -23,6 +23,6 @@
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"quality_scale": "platinum",
"requirements": ["aiohomeconnect==0.28.0"],
"requirements": ["aiohomeconnect==0.26.0"],
"zeroconf": ["_homeconnect._tcp.local."]
}

View File

@@ -15,7 +15,7 @@ set_program_and_options:
- active_program
- selected_program
program:
example: dishcare_dishwasher_program_auto_2
example: dishcare_dishwasher_program_auto2
selector:
select:
mode: dropdown
@@ -73,7 +73,6 @@ 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
@@ -122,7 +121,6 @@ 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
@@ -149,7 +147,6 @@ 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
@@ -177,7 +174,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_spin_drain
- laundry_care_washer_program_spin_drain
- laundry_care_washer_program_sport_fitness
- laundry_care_washer_program_super_153045_super_15
- laundry_care_washer_program_super_153045_super_1530

View File

@@ -240,7 +240,6 @@
"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%]",
@@ -272,7 +271,6 @@
"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%]",
@@ -352,7 +350,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_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
"laundry_care_washer_program_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_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%]",
@@ -594,7 +592,6 @@
"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%]",
@@ -615,7 +612,6 @@
"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%]",
@@ -627,7 +623,6 @@
"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%]",
@@ -707,7 +702,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_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
"laundry_care_washer_program_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_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%]",
@@ -1588,7 +1583,6 @@
"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",
@@ -1609,7 +1603,6 @@
"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",
@@ -1621,7 +1614,6 @@
"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",
@@ -1701,7 +1693,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_spin_drain": "Spin/drain",
"laundry_care_washer_program_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",

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
"integration_type": "system",
"requirements": [
"serialx==0.6.2",
"serialx==0.5.0",
"universal-silabs-flasher==0.1.2",
"ha-silabs-firmware-client==0.3.0"
]

View File

@@ -13,6 +13,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==10.0.1"],
"requirements": ["python-homewizard-energy==10.0.0"],
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
}

View File

@@ -34,7 +34,6 @@ from .const import ( # noqa: F401
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
ATTR_TARGET_HUMIDITY_STEP,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DOMAIN,
@@ -142,7 +141,6 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"min_humidity",
"max_humidity",
"supported_features",
"target_humidity_step",
}
@@ -150,12 +148,7 @@ 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_TARGET_HUMIDITY_STEP,
}
{ATTR_MIN_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_AVAILABLE_MODES}
)
entity_description: HumidifierEntityDescription
@@ -168,7 +161,6 @@ 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]:
@@ -177,8 +169,6 @@ 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
@@ -261,11 +251,6 @@ 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."""

View File

@@ -28,7 +28,6 @@ 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

View File

@@ -19,10 +19,6 @@
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
@@ -31,11 +27,14 @@
- 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:

View File

@@ -12,5 +12,5 @@
"iot_class": "local_polling",
"loggers": ["incomfortclient"],
"quality_scale": "platinum",
"requirements": ["incomfort-client==0.6.11"]
"requirements": ["incomfort-client==0.6.10"]
}

View File

@@ -256,8 +256,6 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
supported_fn=(
lambda coordinator: coordinator.device.dashboard.model_name
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
in coordinator.device.dashboard.config
),
),
LaMarzoccoNumberEntityDescription(
@@ -291,8 +289,6 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
supported_fn=(
lambda coordinator: coordinator.device.dashboard.model_name
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
in coordinator.device.dashboard.config
),
),
)

View File

@@ -149,8 +149,6 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
supported_fn=(
lambda coordinator: coordinator.device.dashboard.model_name
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
in coordinator.device.dashboard.config
),
),
)

View File

@@ -42,7 +42,6 @@ PLATFORMS = [
Platform.CLIMATE,
Platform.EVENT,
Platform.FAN,
Platform.HUMIDIFIER,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,

View File

@@ -1,195 +0,0 @@
"""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)
)

View File

@@ -199,33 +199,6 @@
}
}
},
"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"

View File

@@ -19,10 +19,6 @@
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
@@ -31,6 +27,10 @@
- input_number
- number
- sensor
number:
selector:
number:
mode: box
translation_key: number_or_entity
turned_on: *trigger_common
@@ -48,7 +48,6 @@ brightness_crossed_threshold:
behavior: *trigger_behavior
threshold_type:
required: true
default: above
selector:
select:
options:

View File

@@ -66,9 +66,8 @@ class MatterRangeNumberEntityDescription(
format_max_value: Callable[[float], float] = lambda x: x
# command: a custom callback to create the command to send to the device
# the callback's argument will be the converted device value from ha_to_device
# if omitted the command will just be a write_attribute command to the primary attribute
command: Callable[[int], ClusterCommand] | None = None
# the callback's argument will be the index of the selected list value
command: Callable[[int], ClusterCommand]
class MatterNumber(MatterEntity, NumberEntity):
@@ -100,15 +99,9 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
send_value = self.entity_description.ha_to_device(value)
if self.entity_description.command:
# custom command defined to set the new value
await self.send_device_command(
self.entity_description.command(send_value),
)
return
# regular write attribute to set the new value
await self.write_attribute(
value=send_value,
# custom command defined to set the new value
await self.send_device_command(
self.entity_description.command(send_value),
)
@callback
@@ -260,30 +253,6 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterNumber,
required_attributes=(custom_clusters.EveCluster.Attributes.Altitude,),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterRangeNumberEntityDescription(
key="ThermostatOccupiedSetback",
entity_category=EntityCategory.CONFIG,
translation_key="occupied_setback",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_to_ha=lambda x: None if x is None else x / 10,
ha_to_device=lambda x: round(x * 10),
format_min_value=lambda x: x / 10,
format_max_value=lambda x: x / 10,
min_attribute=clusters.Thermostat.Attributes.OccupiedSetbackMin,
max_attribute=clusters.Thermostat.Attributes.OccupiedSetbackMax,
native_step=0.5,
mode=NumberMode.BOX,
),
entity_class=MatterRangeNumber,
required_attributes=(
clusters.Thermostat.Attributes.OccupiedSetback,
clusters.Thermostat.Attributes.OccupiedSetbackMin,
clusters.Thermostat.Attributes.OccupiedSetbackMax,
),
featuremap_contains=(clusters.Thermostat.Bitmaps.Feature.kSetback),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterNumberEntityDescription(

View File

@@ -173,13 +173,6 @@ EVSE_FAULT_STATE_MAP = {
clusters.EnergyEvse.Enums.FaultStateEnum.kOther: "other",
}
SETPOINT_CHANGE_SOURCE_MAP = {
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kManual: "manual",
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kSchedule: "schedule",
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kExternal: "external",
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kUnknownEnumValue: None,
}
PUMP_CONTROL_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantSpeed: "constant_speed",
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantPressure: "constant_pressure",
@@ -1548,48 +1541,4 @@ DISCOVERY_SCHEMAS = [
required_attributes=(clusters.DoorLock.Attributes.DoorClosedEvents,),
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="SetpointChangeSource",
translation_key="setpoint_change_source",
device_class=SensorDeviceClass.ENUM,
state_class=None,
options=[x for x in SETPOINT_CHANGE_SOURCE_MAP.values() if x is not None],
device_to_ha=lambda x: SETPOINT_CHANGE_SOURCE_MAP[x],
),
entity_class=MatterSensor,
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeSource,),
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="SetpointChangeSourceTimestamp",
translation_key="setpoint_change_timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=None,
device_to_ha=matter_epoch_seconds_to_utc,
),
entity_class=MatterSensor,
required_attributes=(
clusters.Thermostat.Attributes.SetpointChangeSourceTimestamp,
),
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ThermostatSetpointChangeAmount",
translation_key="setpoint_change_amount",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=1,
device_class=SensorDeviceClass.TEMPERATURE,
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeAmount,),
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
),
]

View File

@@ -217,9 +217,6 @@
"led_indicator_intensity_on": {
"name": "LED on intensity"
},
"occupied_setback": {
"name": "Occupied setback"
},
"off_transition_time": {
"name": "Off transition time"
},
@@ -549,20 +546,6 @@
"rms_voltage": {
"name": "Effective voltage"
},
"setpoint_change_amount": {
"name": "Last change amount"
},
"setpoint_change_source": {
"name": "Last change source",
"state": {
"external": "External",
"manual": "Manual",
"schedule": "Schedule"
}
},
"setpoint_change_timestamp": {
"name": "Last change"
},
"switch_current_position": {
"name": "Current switch position"
},

View File

@@ -1,6 +1,6 @@
{
"domain": "namecheapdns",
"name": "Namecheap DynamicDNS",
"name": "Namecheap FreeDNS",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/namecheapdns",
"iot_class": "cloud_push",

View File

@@ -1,9 +1,5 @@
"""Support for Netatmo binary sensors."""
from dataclasses import dataclass
import logging
from typing import Final, cast
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
@@ -13,33 +9,17 @@ 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_BINARY_SENSOR
from .const import NETATMO_CREATE_WEATHER_SENSOR
from .data_handler import NetatmoDevice
from .entity import NetatmoWeatherModuleEntity
_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(
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="reachable",
name="Connectivity",
netatmo_name="reachable",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
]
)
async def async_setup_entry(
@@ -47,75 +27,36 @@ async def async_setup_entry(
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Netatmo weather binary sensors based on a config entry."""
"""Set up Netatmo binary sensors based on a config entry."""
@callback
def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None:
"""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)
async_add_entities(
NetatmoWeatherBinarySensor(netatmo_device, description)
for description in BINARY_SENSOR_TYPES
if description.key in netatmo_device.device.features
)
entry.async_on_unload(
async_dispatcher_connect(
hass,
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
_create_weather_binary_sensor_entity,
hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_binary_sensor_entity
)
)
class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity):
"""Implementation of a Netatmo weather binary sensor."""
entity_description: NetatmoBinarySensorEntityDescription
"""Implementation of a Netatmo binary sensor."""
def __init__(
self,
netatmo_device: NetatmoDevice,
description: NetatmoBinarySensorEntityDescription,
self, device: NetatmoDevice, description: BinarySensorEntityDescription
) -> None:
"""Initialize a Netatmo weather binary sensor."""
super().__init__(netatmo_device)
"""Initialize a Netatmo binary sensor."""
super().__init__(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."""
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._attr_is_on = self.device.reachable
self.async_write_ha_state()

View File

@@ -53,7 +53,6 @@ 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"

View File

@@ -45,7 +45,6 @@ from .const import (
NETATMO_CREATE_SELECT,
NETATMO_CREATE_SENSOR,
NETATMO_CREATE_SWITCH,
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
NETATMO_CREATE_WEATHER_SENSOR,
PLATFORMS,
WEBHOOK_ACTIVATION,
@@ -333,20 +332,16 @@ class NetatmoDataHandler:
"""Set up home coach/air care modules."""
for module in self.account.modules.values():
if module.device_category is NetatmoDeviceCategory.air_care:
for signal in (
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
async_dispatcher_send(
self.hass,
NETATMO_CREATE_WEATHER_SENSOR,
):
async_dispatcher_send(
self.hass,
signal,
NetatmoDevice(
self,
module,
AIR_CARE,
AIR_CARE,
),
)
NetatmoDevice(
self,
module,
AIR_CARE,
AIR_CARE,
),
)
def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None:
"""Set up modules."""
@@ -384,20 +379,16 @@ class NetatmoDataHandler:
),
)
if module.device_category is NetatmoDeviceCategory.weather:
for signal in (
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
async_dispatcher_send(
self.hass,
NETATMO_CREATE_WEATHER_SENSOR,
):
async_dispatcher_send(
self.hass,
signal,
NetatmoDevice(
self,
module,
home.entity_id,
WEATHER,
),
)
NetatmoDevice(
self,
module,
home.entity_id,
WEATHER,
),
)
def setup_rooms(self, home: pyatmo.Home, signal_home: str) -> None:
"""Set up rooms."""

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