mirror of
https://github.com/home-assistant/core.git
synced 2026-01-12 18:47:17 +01:00
Compare commits
3 Commits
dev
...
disable_py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9330ec6a03 | ||
|
|
fdefbf68ad | ||
|
|
193c02a253 |
@@ -40,7 +40,8 @@
|
||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
|
||||
4
.vscode/settings.default.jsonc
vendored
4
.vscode/settings.default.jsonc
vendored
@@ -7,8 +7,8 @@
|
||||
"python.testing.pytestEnabled": false,
|
||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
// Pyright is too pedantic for Home Assistant
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"recalibrate_co2": {
|
||||
"default": "mdi:molecule-co2"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"hysteresis_band": {
|
||||
"default": "mdi:delta"
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairobotrest"],
|
||||
"quality_scale": "gold",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyairobotrest==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,11 +59,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"recalibrate_co2": {
|
||||
"name": "Recalibrate CO2 sensor"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"hysteresis_band": {
|
||||
"name": "Hysteresis band"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==1.0.5"]
|
||||
"requirements": ["aioairzone==1.0.4"]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"] != (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
||||
|
||||
@@ -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()
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -98,15 +97,4 @@ async def async_get_config_entry_diagnostics(
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}",
|
||||
)
|
||||
|
||||
@@ -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*",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.8.0"],
|
||||
"requirements": ["hass-nabucasa==1.7.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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](
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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](
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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](
|
||||
|
||||
@@ -764,7 +764,7 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="available_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
value_fn=attrgetter("available_energy"),
|
||||
),
|
||||
EnvoyEnchargeAggregateSensorEntityDescription(
|
||||
@@ -772,14 +772,14 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="reserve_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY_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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260107.1"]
|
||||
"requirements": ["home-assistant-frontend==20260107.0"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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%]"
|
||||
},
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ from homeassistant.core import HomeAssistant
|
||||
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.SELECT,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"issue_hotplug": {
|
||||
"default": "mdi:connection"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"opmode": {
|
||||
"default": "mdi:cogs"
|
||||
@@ -15,38 +10,6 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
|
||||
@@ -19,11 +19,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"issue_hotplug": {
|
||||
"name": "Issue hotplug"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"opmode": {
|
||||
"name": "Operation mode",
|
||||
@@ -56,38 +51,6 @@
|
||||
"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": {
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhik"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyHik==0.4.0"]
|
||||
"requirements": ["pyHik==0.3.4"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -42,7 +42,6 @@ PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
Platform.EVENT,
|
||||
Platform.FAN,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -217,9 +217,6 @@
|
||||
"led_indicator_intensity_on": {
|
||||
"name": "LED on intensity"
|
||||
},
|
||||
"occupied_setback": {
|
||||
"name": "Occupied setback"
|
||||
},
|
||||
"off_transition_time": {
|
||||
"name": "Off transition time"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -256,7 +256,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
if slot_id > user_input[CONF_MESSAGE_SLOTS]
|
||||
]
|
||||
|
||||
removed_entities_area = [
|
||||
removed_entites_area = [
|
||||
f"{cfg_region}-{slot_id}"
|
||||
for slot_id in range(1, self.data[CONF_MESSAGE_SLOTS] + 1)
|
||||
for cfg_region in self.data[CONF_REGIONS]
|
||||
@@ -265,7 +265,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
|
||||
for entry in entries:
|
||||
for entity_uid in list(
|
||||
set(removed_entities_slots + removed_entities_area)
|
||||
set(removed_entities_slots + removed_entites_area)
|
||||
):
|
||||
if entry.unique_id == entity_uid:
|
||||
entity_registry.async_remove(entry.entity_id)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynina"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynina==0.3.6"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration does not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use services that need authentication.
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Patch the library instead of the HTTP requests
|
||||
Create a shared fixture for the mock config entry
|
||||
Use init_integration in tests
|
||||
Evaluate the need of test_config_entry_not_ready
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does use a cloud service.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does use a cloud service.
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use devices.
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: done
|
||||
entity-category: todo
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: |
|
||||
Extract attributes into own entities.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not custom icons.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use issues.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use devices.
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoauth", "pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.2"]
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.0"]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ DEVICE_SUPPORT = {
|
||||
"3A": (),
|
||||
"3B": (),
|
||||
"42": (),
|
||||
"7E": ("EDS0065", "EDS0066", "EDS0068"),
|
||||
"7E": ("EDS0066", "EDS0068"),
|
||||
"A6": (),
|
||||
"EF": ("HB_HUB", "HB_MOISTURE_METER", "HobbyBoards_EF"),
|
||||
}
|
||||
|
||||
@@ -297,20 +297,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
|
||||
# 7E sensors are special sensors by Embedded Data Systems
|
||||
|
||||
EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
|
||||
"EDS0065": (
|
||||
OneWireSensorEntityDescription(
|
||||
key="EDS0065/temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
OneWireSensorEntityDescription(
|
||||
key="EDS0065/humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
),
|
||||
"EDS0066": (
|
||||
OneWireSensorEntityDescription(
|
||||
key="EDS0066/temperature",
|
||||
|
||||
@@ -17,8 +17,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
ATTR_SERIAL_NUMBER,
|
||||
CONF_HOST,
|
||||
CONF_MONITORED_VARIABLES,
|
||||
UnitOfEnergy,
|
||||
@@ -28,7 +26,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@@ -43,25 +40,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="status",
|
||||
translation_key="status",
|
||||
name="Charging Status",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="charge_time",
|
||||
translation_key="charge_time",
|
||||
name="Charge Time Elapsed",
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="ambient_temp",
|
||||
translation_key="ambient_temp",
|
||||
name="Ambient Temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="ir_temp",
|
||||
translation_key="ir_temp",
|
||||
name="IR Temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -69,7 +66,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="rtc_temp",
|
||||
translation_key="rtc_temp",
|
||||
name="RTC Temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -77,14 +74,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="usage_session",
|
||||
translation_key="usage_session",
|
||||
name="Usage this Session",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="usage_total",
|
||||
translation_key="usage_total",
|
||||
name="Total Usage",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@@ -161,10 +158,9 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
(
|
||||
OpenEVSESensor(
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.runtime_data,
|
||||
description,
|
||||
config_entry.entry_id,
|
||||
config_entry.unique_id,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
),
|
||||
@@ -175,32 +171,17 @@ async def async_setup_entry(
|
||||
class OpenEVSESensor(SensorEntity):
|
||||
"""Implementation of an OpenEVSE sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
charger: OpenEVSE,
|
||||
description: SensorEntityDescription,
|
||||
entry_id: str,
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self.host = host
|
||||
self.charger = charger
|
||||
|
||||
identifier = unique_id or entry_id
|
||||
self._attr_unique_id = f"{identifier}-{description.key}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, identifier)},
|
||||
manufacturer="OpenEVSE",
|
||||
)
|
||||
if unique_id:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(CONNECTION_NETWORK_MAC, unique_id)
|
||||
}
|
||||
self._attr_device_info[ATTR_SERIAL_NUMBER] = unique_id
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the monitored data from the charger."""
|
||||
try:
|
||||
|
||||
@@ -18,31 +18,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ambient_temp": {
|
||||
"name": "Ambient temperature"
|
||||
},
|
||||
"charge_time": {
|
||||
"name": "Charge time elapsed"
|
||||
},
|
||||
"ir_temp": {
|
||||
"name": "IR temperature"
|
||||
},
|
||||
"rtc_temp": {
|
||||
"name": "RTC temperature"
|
||||
},
|
||||
"status": {
|
||||
"name": "Charging status"
|
||||
},
|
||||
"usage_session": {
|
||||
"name": "Usage this session"
|
||||
},
|
||||
"usage_total": {
|
||||
"name": "Total energy usage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"yaml_deprecated": {
|
||||
"description": "Configuring OpenEVSE using YAML is being removed. Your existing YAML configuration has been imported into the UI automatically. Remove the `openevse` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.16.1"]
|
||||
"requirements": ["opower==0.16.0"]
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"state": {
|
||||
"advanced": "Advanced",
|
||||
"gridcompany": "Grid company",
|
||||
"nettleie": "Grid fee",
|
||||
"nettleie": "Nettleie",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"oso": "OSO",
|
||||
"smartcompany": "Smart company"
|
||||
|
||||
@@ -128,15 +128,15 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
|
||||
|
||||
states = self.device.states
|
||||
|
||||
if (
|
||||
state := states[OverkizState.IO_TARGET_HEATING_LEVEL]
|
||||
) and state.value_as_str:
|
||||
return OVERKIZ_TO_PRESET_MODE[state.value_as_str]
|
||||
|
||||
if (
|
||||
operating_mode := states[OverkizState.CORE_OPERATING_MODE]
|
||||
) and operating_mode.value_as_str == OverkizCommandParam.EXTERNAL:
|
||||
return PRESET_EXTERNAL
|
||||
|
||||
if (
|
||||
state := states[OverkizState.IO_TARGET_HEATING_LEVEL]
|
||||
) and state.value_as_str:
|
||||
return OVERKIZ_TO_PRESET_MODE[state.value_as_str]
|
||||
return None
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
|
||||
@@ -7,6 +7,7 @@ from aiohttp import ClientError, ClientResponseError, web
|
||||
from pypoint import PointSession
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_WEBHOOK_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
@@ -20,12 +21,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from . import api
|
||||
from .const import CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, SIGNAL_WEBHOOK
|
||||
from .coordinator import PointConfigEntry, PointDataUpdateCoordinator
|
||||
from .coordinator import PointDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
type PointConfigEntry = ConfigEntry[PointDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bool:
|
||||
"""Set up Minut Point from a config entry."""
|
||||
@@ -56,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> boo
|
||||
|
||||
point_session = PointSession(auth)
|
||||
|
||||
coordinator = PointDataUpdateCoordinator(hass, point_session, entry)
|
||||
coordinator = PointDataUpdateCoordinator(hass, point_session)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PointConfigEntry
|
||||
from .const import DOMAIN, SIGNAL_WEBHOOK
|
||||
from .coordinator import PointConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PointConfigEntry
|
||||
from .const import SIGNAL_WEBHOOK
|
||||
from .coordinator import PointConfigEntry, PointDataUpdateCoordinator
|
||||
from .coordinator import PointDataUpdateCoordinator
|
||||
from .entity import MinutPointEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
|
||||
from pypoint import PointSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
@@ -16,24 +15,17 @@ from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type PointConfigEntry = ConfigEntry[PointDataUpdateCoordinator]
|
||||
|
||||
|
||||
class PointDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
"""Class to manage fetching Point data from the API."""
|
||||
|
||||
config_entry: PointConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, point: PointSession, config_entry: PointConfigEntry
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, point: PointSession) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.point = point
|
||||
self.device_updates: dict[str, datetime] = {}
|
||||
|
||||
@@ -14,7 +14,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import PointConfigEntry, PointDataUpdateCoordinator
|
||||
from . import PointConfigEntry
|
||||
from .coordinator import PointDataUpdateCoordinator
|
||||
from .entity import MinutPointEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pooldose",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "gold",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-pooldose==0.8.1"]
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ 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
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: This integration does not support dynamic device discovery, as each config entry represents a single PoolDose device with all available entities.
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyportainer import Portainer
|
||||
@@ -33,40 +30,23 @@ from .coordinator import (
|
||||
PortainerCoordinator,
|
||||
PortainerCoordinatorData,
|
||||
)
|
||||
from .entity import PortainerContainerEntity, PortainerEndpointEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .entity import PortainerContainerEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PortainerButtonDescription(ButtonEntityDescription):
|
||||
"""Class to describe a Portainer button entity."""
|
||||
|
||||
# Note to reviewer: I am keeping the third argument a str, in order to keep mypy happy :)
|
||||
press_action: Callable[
|
||||
[Portainer, int, str],
|
||||
Coroutine[Any, Any, None],
|
||||
]
|
||||
|
||||
|
||||
ENDPOINT_BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
PortainerButtonDescription(
|
||||
key="images_prune",
|
||||
translation_key="images_prune",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action=(
|
||||
lambda portainer, endpoint_id, _: portainer.images_prune(
|
||||
endpoint_id=endpoint_id, dangling=False, until=timedelta(days=0)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
CONTAINER_BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
PortainerButtonDescription(
|
||||
key="restart",
|
||||
translation_key="restart_container",
|
||||
name="Restart Container",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action=(
|
||||
@@ -86,43 +66,22 @@ async def async_setup_entry(
|
||||
"""Set up Portainer buttons."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def _async_add_new_endpoints(endpoints: list[PortainerCoordinatorData]) -> None:
|
||||
"""Add new endpoint binary sensors."""
|
||||
async_add_entities(
|
||||
PortainerEndpointButton(
|
||||
coordinator,
|
||||
entity_description,
|
||||
endpoint,
|
||||
)
|
||||
for entity_description in ENDPOINT_BUTTONS
|
||||
for endpoint in endpoints
|
||||
)
|
||||
|
||||
def _async_add_new_containers(
|
||||
containers: list[tuple[PortainerCoordinatorData, PortainerContainerData]],
|
||||
) -> None:
|
||||
"""Add new container button sensors."""
|
||||
async_add_entities(
|
||||
PortainerContainerButton(
|
||||
PortainerButton(
|
||||
coordinator,
|
||||
entity_description,
|
||||
container,
|
||||
endpoint,
|
||||
)
|
||||
for (endpoint, container) in containers
|
||||
for entity_description in CONTAINER_BUTTONS
|
||||
for entity_description in BUTTONS
|
||||
)
|
||||
|
||||
coordinator.new_endpoints_callbacks.append(_async_add_new_endpoints)
|
||||
coordinator.new_containers_callbacks.append(_async_add_new_containers)
|
||||
|
||||
_async_add_new_endpoints(
|
||||
[
|
||||
endpoint
|
||||
for endpoint in coordinator.data.values()
|
||||
if endpoint.id in coordinator.known_endpoints
|
||||
]
|
||||
)
|
||||
_async_add_new_containers(
|
||||
[
|
||||
(endpoint, container)
|
||||
@@ -132,62 +91,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class PortainerBaseButton(ButtonEntity):
|
||||
"""Common base for Portainer buttons. Basically to ensure the async_press logic isn't duplicated."""
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
coordinator: PortainerCoordinator
|
||||
|
||||
@abstractmethod
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Abstract method used per Portainer button class."""
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger the Portainer button press service."""
|
||||
try:
|
||||
await self._async_press_call()
|
||||
except PortainerConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect_no_details",
|
||||
) from err
|
||||
except PortainerAuthenticationError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth_no_details",
|
||||
) from err
|
||||
except PortainerTimeoutError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_connect_no_details",
|
||||
) from err
|
||||
|
||||
|
||||
class PortainerEndpointButton(PortainerEndpointEntity, PortainerBaseButton):
|
||||
"""Defines a Portainer endpoint button."""
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerButtonDescription,
|
||||
device_info: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer endpoint button entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Call the endpoint button press action."""
|
||||
await self.entity_description.press_action(
|
||||
self.coordinator.portainer, self.device_id, ""
|
||||
)
|
||||
|
||||
|
||||
class PortainerContainerButton(PortainerContainerEntity, PortainerBaseButton):
|
||||
class PortainerButton(PortainerContainerEntity, ButtonEntity):
|
||||
"""Defines a Portainer button."""
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
@@ -205,10 +109,29 @@ class PortainerContainerButton(PortainerContainerEntity, PortainerBaseButton):
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Call the container button press action."""
|
||||
await self.entity_description.press_action(
|
||||
self.coordinator.portainer,
|
||||
self.endpoint_id,
|
||||
self.container_data.container.id,
|
||||
)
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger the Portainer button press service."""
|
||||
try:
|
||||
await self.entity_description.press_action(
|
||||
self.coordinator.portainer,
|
||||
self.endpoint_id,
|
||||
self.container_data.container.id,
|
||||
)
|
||||
except PortainerConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except PortainerAuthenticationError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except PortainerTimeoutError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
@@ -61,14 +61,6 @@
|
||||
"name": "Status"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"images_prune": {
|
||||
"name": "Prune unused images"
|
||||
},
|
||||
"restart_container": {
|
||||
"name": "Restart container"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"api_version": {
|
||||
"name": "API version"
|
||||
@@ -146,20 +138,11 @@
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while trying to connect to the Portainer instance: {error}"
|
||||
},
|
||||
"cannot_connect_no_details": {
|
||||
"message": "An error occurred while trying to connect to the Portainer instance."
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "An error occurred while trying to authenticate: {error}"
|
||||
},
|
||||
"invalid_auth_no_details": {
|
||||
"message": "An error occurred while trying to authenticate."
|
||||
},
|
||||
"timeout_connect": {
|
||||
"message": "A timeout occurred while trying to connect to the Portainer instance: {error}"
|
||||
},
|
||||
"timeout_connect_no_details": {
|
||||
"message": "A timeout occurred while trying to connect to the Portainer instance."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,12 @@ class PortainerSwitchEntityDescription(SwitchEntityDescription):
|
||||
async def perform_action(
|
||||
action: str, portainer: Portainer, endpoint_id: int, container_id: str
|
||||
) -> None:
|
||||
"""Perform an action on a container."""
|
||||
"""Stop a container."""
|
||||
try:
|
||||
match action:
|
||||
case "start":
|
||||
await portainer.start_container(endpoint_id, container_id)
|
||||
case "stop":
|
||||
await portainer.stop_container(endpoint_id, container_id)
|
||||
if action == "start":
|
||||
await portainer.start_container(endpoint_id, container_id)
|
||||
elif action == "stop":
|
||||
await portainer.stop_container(endpoint_id, container_id)
|
||||
except PortainerAuthenticationError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -151,12 +151,6 @@ SENSOR_DESCRIPTIONS = {
|
||||
translation_key="nox_index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"iaqs": SensorEntityDescription(
|
||||
key="iaqs",
|
||||
translation_key="iaqs",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@
|
||||
"acceleration_z": {
|
||||
"name": "Acceleration Z"
|
||||
},
|
||||
"iaqs": {
|
||||
"name": "Indoor air quality score"
|
||||
},
|
||||
"movement_counter": {
|
||||
"name": "Movement counter"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user