Compare commits

..

1 Commits

Author SHA1 Message Date
jbouwh
d09c879b10 Fix tuya energy sensor units 2026-01-06 23:50:03 +00:00
303 changed files with 2020 additions and 12277 deletions

7
CODEOWNERS generated
View File

@@ -661,8 +661,6 @@ build.json @home-assistant/supervisor
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
/homeassistant/components/hassio/ @home-assistant/supervisor
/tests/components/hassio/ @home-assistant/supervisor
/homeassistant/components/hdfury/ @glenndehaan
/tests/components/hdfury/ @glenndehaan
/homeassistant/components/hdmi_cec/ @inytar
/tests/components/hdmi_cec/ @inytar
/homeassistant/components/heatmiser/ @andylockran
@@ -1172,8 +1170,8 @@ build.json @home-assistant/supervisor
/tests/components/open_router/ @joostlek
/homeassistant/components/openerz/ @misialq
/tests/components/openerz/ @misialq
/homeassistant/components/openevse/ @c00w @firstof9
/tests/components/openevse/ @c00w @firstof9
/homeassistant/components/openevse/ @c00w
/tests/components/openevse/ @c00w
/homeassistant/components/openexchangerates/ @MartinHjelmare
/tests/components/openexchangerates/ @MartinHjelmare
/homeassistant/components/opengarage/ @danielhiversen
@@ -1805,7 +1803,6 @@ build.json @home-assistant/supervisor
/tests/components/waqi/ @joostlek
/homeassistant/components/water_heater/ @home-assistant/core
/tests/components/water_heater/ @home-assistant/core
/homeassistant/components/waterfurnace/ @sdague @masterkoppa
/homeassistant/components/watergate/ @adam-the-hero
/tests/components/watergate/ @adam-the-hero
/homeassistant/components/watson_tts/ @rutkai

View File

@@ -43,13 +43,6 @@ BUTTON_TYPES: tuple[AirobotButtonEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
press_fn=lambda coordinator: coordinator.client.reboot_thermostat(),
),
AirobotButtonEntityDescription(
key="recalibrate_co2",
translation_key="recalibrate_co2",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
press_fn=lambda coordinator: coordinator.client.recalibrate_co2_sensor(),
),
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,7 +69,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.json import json_dumps
from homeassistant.util import slugify
from . import AnthropicConfigEntry
@@ -194,7 +193,7 @@ def _convert_content(
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json_dumps(content.tool_result),
content=json.dumps(content.tool_result),
)
external_tool = False
if not messages or messages[-1]["role"] != (

View File

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

View File

@@ -7,7 +7,7 @@ import asyncio
from collections.abc import Callable, Mapping
from dataclasses import dataclass
import logging
from typing import Any, Literal, Protocol, cast
from typing import Any, Protocol, cast
from propcache.api import cached_property
import voluptuous as vol
@@ -16,10 +16,7 @@ from homeassistant.components import labs, websocket_api
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.components.labs import async_listen as async_labs_listen
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_ENTITY_ID,
ATTR_FLOOR_ID,
ATTR_LABEL_ID,
ATTR_MODE,
ATTR_NAME,
CONF_ACTIONS,
@@ -33,7 +30,6 @@ from homeassistant.const import (
CONF_OPTIONS,
CONF_PATH,
CONF_PLATFORM,
CONF_TARGET,
CONF_TRIGGERS,
CONF_VARIABLES,
CONF_ZONE,
@@ -593,32 +589,20 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return True if entity is on."""
return self._async_detach_triggers is not None or self._is_enabled
@cached_property
@property
def referenced_labels(self) -> set[str]:
"""Return a set of referenced labels."""
referenced = self.action_script.referenced_labels
return self.action_script.referenced_labels
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
return referenced
@cached_property
@property
def referenced_floors(self) -> set[str]:
"""Return a set of referenced floors."""
referenced = self.action_script.referenced_floors
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
return referenced
return self.action_script.referenced_floors
@cached_property
def referenced_areas(self) -> set[str]:
"""Return a set of referenced areas."""
referenced = self.action_script.referenced_areas
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
return referenced
return self.action_script.referenced_areas
@property
def referenced_blueprint(self) -> str | None:
@@ -1226,9 +1210,6 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]
if target_devices := _get_targets_from_trigger_config(trigger_conf, CONF_DEVICE_ID):
return target_devices
return []
@@ -1259,28 +1240,9 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
):
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
if target_entities := _get_targets_from_trigger_config(
trigger_conf, CONF_ENTITY_ID
):
return target_entities
return []
@callback
def _get_targets_from_trigger_config(
config: dict,
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
) -> list[str]:
"""Extract targets from a target config."""
if not (target_conf := config.get(CONF_TARGET)):
return []
if not (targets := target_conf.get(target)):
return []
return [targets] if isinstance(targets, str) else targets
@websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
def websocket_config(
hass: HomeAssistant,

View File

@@ -36,10 +36,6 @@ _LOGGER = logging.getLogger(__name__)
# Cache TTL for backup list (in seconds)
CACHE_TTL = 300
# Timeout for upload operations (in seconds)
# This prevents uploads from hanging indefinitely
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
"""Return the suggested filenames for the backup and metadata files."""
@@ -333,28 +329,13 @@ class BackblazeBackupAgent(BackupAgent):
_LOGGER.debug("Uploading backup file %s with streaming", filename)
try:
content_type, _ = mimetypes.guess_type(filename)
file_version = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._upload_unbound_stream_sync,
reader,
filename,
content_type or "application/x-tar",
file_info,
),
timeout=UPLOAD_TIMEOUT,
file_version = await self._hass.async_add_executor_job(
self._upload_unbound_stream_sync,
reader,
filename,
content_type or "application/x-tar",
file_info,
)
except TimeoutError:
_LOGGER.error(
"Upload of %s timed out after %s seconds", filename, UPLOAD_TIMEOUT
)
reader.abort()
raise BackupAgentError(
f"Upload timed out after {UPLOAD_TIMEOUT} seconds"
) from None
except asyncio.CancelledError:
_LOGGER.warning("Upload of %s was cancelled", filename)
reader.abort()
raise
finally:
reader.close()

View File

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

View File

@@ -1,63 +0,0 @@
"""Binary Sensor entities for the Bang & Olufsen integration."""
from __future__ import annotations
from mozart_api.models import BatteryState
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
from .entity import BeoEntity
from .util import supports_battery
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Binary Sensor entities from config entry."""
if await supports_battery(config_entry.runtime_data.client):
async_add_entities(new_entities=[BeoBinarySensorBatteryCharging(config_entry)])
class BeoBinarySensorBatteryCharging(BinarySensorEntity, BeoEntity):
"""Battery charging Binary Sensor."""
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
_attr_is_on = False
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Init the battery charging Binary Sensor."""
super().__init__(config_entry, config_entry.runtime_data.client)
self._attr_unique_id = f"{self._unique_id}_charging"
async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
self._update_battery_charging,
)
)
async def _update_battery_charging(self, data: BatteryState) -> None:
"""Update battery charging."""
self._attr_is_on = bool(data.is_charging)
self.async_write_ha_state()

View File

@@ -115,7 +115,6 @@ class WebsocketNotification(StrEnum):
"""Enum for WebSocket notification types."""
ACTIVE_LISTENING_MODE = "active_listening_mode"
BATTERY = "battery"
BEO_REMOTE_BUTTON = "beo_remote_button"
BUTTON = "button"
PLAYBACK_ERROR = "playback_error"

View File

@@ -4,10 +4,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -57,19 +55,6 @@ async def async_get_config_entry_diagnostics(
# Get remotes
for remote in await get_remotes(config_entry.runtime_data.client):
# Get Battery Sensor states
if entity_id := entity_registry.async_get_entity_id(
SENSOR_DOMAIN,
DOMAIN,
f"{remote.serial_number}_{config_entry.unique_id}_remote_battery_level",
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data[f"remote_{remote.serial_number}_battery_level"] = state_dict
# Get key Event entity states (if enabled)
for key_type in get_remote_keys():
if entity_id := entity_registry.async_get_entity_id(
@@ -87,26 +72,4 @@ async def async_get_config_entry_diagnostics(
# Add remote Mozart model
data[f"remote_{remote.serial_number}"] = dict(remote)
# Get Mozart battery entity
if entity_id := entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_battery_level"
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data["battery_level"] = state_dict
# Get Mozart battery charging entity
if entity_id := entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_charging"
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data["charging"] = state_dict
return data

View File

@@ -1,139 +0,0 @@
"""Sensor entities for the Bang & Olufsen integration."""
from __future__ import annotations
import contextlib
from datetime import timedelta
from aiohttp import ClientConnectorError
from mozart_api.exceptions import ApiException
from mozart_api.models import BatteryState, PairedRemote
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
from .entity import BeoEntity
from .util import get_remotes, supports_battery
SCAN_INTERVAL = timedelta(minutes=15)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sensor entities from config entry."""
entities: list[BeoSensor] = []
# Check for Mozart device with battery
if await supports_battery(config_entry.runtime_data.client):
entities.append(BeoSensorBatteryLevel(config_entry))
# Add any Beoremote One remotes
entities.extend(
[
BeoSensorRemoteBatteryLevel(config_entry, remote)
for remote in (await get_remotes(config_entry.runtime_data.client))
]
)
async_add_entities(entities, update_before_add=True)
class BeoSensor(SensorEntity, BeoEntity):
"""Base Bang & Olufsen Sensor."""
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Initialize Sensor."""
super().__init__(config_entry, config_entry.runtime_data.client)
class BeoSensorBatteryLevel(BeoSensor):
"""Battery level Sensor for Mozart devices."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Init the battery level Sensor."""
super().__init__(config_entry)
self._attr_unique_id = f"{self._unique_id}_battery_level"
async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
self._update_battery,
)
)
async def _update_battery(self, data: BatteryState) -> None:
"""Update sensor value."""
self._attr_native_value = data.battery_level
self.async_write_ha_state()
class BeoSensorRemoteBatteryLevel(BeoSensor):
"""Battery level Sensor for the Beoremote One."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_should_poll = True
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, config_entry: BeoConfigEntry, remote: PairedRemote) -> None:
"""Init the battery level Sensor."""
super().__init__(config_entry)
# Serial number is not None, as the remote object is provided by get_remotes
assert remote.serial_number
self._attr_unique_id = (
f"{remote.serial_number}_{self._unique_id}_remote_battery_level"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")}
)
self._attr_native_value = remote.battery_level
self._remote = remote
async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
async def async_update(self) -> None:
"""Poll battery status."""
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
for remote in await get_remotes(self._client):
if remote.serial_number == self._remote.serial_number:
self._attr_native_value = remote.battery_level
break

View File

@@ -84,10 +84,3 @@ def get_remote_keys() -> list[str]:
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
],
]
async def supports_battery(client: MozartClient) -> bool:
"""Get if a Mozart device has a battery."""
battery_state = await client.get_battery_state()
return battery_state.state != "BatteryNotPresent"

View File

@@ -6,7 +6,6 @@ import logging
from typing import TYPE_CHECKING
from mozart_api.models import (
BatteryState,
BeoRemoteButton,
ButtonEvent,
ListeningModeProps,
@@ -61,7 +60,6 @@ class BeoWebsocket(BeoBase):
self._client.get_active_listening_mode_notifications(
self.on_active_listening_mode
)
self._client.get_battery_notifications(self.on_battery_notification)
self._client.get_beo_remote_button_notifications(
self.on_beo_remote_button_notification
)
@@ -117,14 +115,6 @@ class BeoWebsocket(BeoBase):
notification,
)
def on_battery_notification(self, notification: BatteryState) -> None:
"""Send battery dispatch."""
async_dispatcher_send(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
notification,
)
def on_beo_remote_button_notification(self, notification: BeoRemoteButton) -> None:
"""Send beo_remote_button dispatch."""
if TYPE_CHECKING:

View File

@@ -22,7 +22,7 @@ from homeassistant.components.media_player import MediaType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_PIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -56,31 +56,8 @@ def catch_braviatv_errors[_BraviaTVCoordinatorT: BraviaTVCoordinator, **_P](
"""Catch Bravia errors and log message."""
try:
await func(self, *args, **kwargs)
except BraviaNotFound as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error_not_found",
translation_placeholders={
"device": self.config_entry.title,
},
) from err
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error_offline",
translation_placeholders={
"device": self.config_entry.title,
},
) from err
except BraviaError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error",
translation_placeholders={
"device": self.config_entry.title,
"error": repr(err),
},
) from err
_LOGGER.error("Command error: %s", err)
await self.async_request_refresh()
return wrapper
@@ -188,35 +165,17 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
if self.skipped_updates < 10:
self.connected = False
self.skipped_updates += 1
_LOGGER.debug(
"Update for %s skipped: the Bravia API service is reloading",
self.config_entry.title,
)
_LOGGER.debug("Update skipped, Bravia API service is reloading")
return
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error_not_found",
translation_placeholders={
"device": self.config_entry.title,
},
) from err
raise UpdateFailed("Error communicating with device") from err
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff):
self.is_on = False
self.connected = False
_LOGGER.debug(
"Update for %s skipped: the TV is turned off", self.config_entry.title
)
_LOGGER.debug("Update skipped, Bravia TV is off")
except BraviaError as err:
self.is_on = False
self.connected = False
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={
"device": self.config_entry.title,
"error": repr(err),
},
) from err
raise UpdateFailed("Error communicating with device") from err
async def async_update_volume(self) -> None:
"""Update volume information."""

View File

@@ -55,22 +55,5 @@
"name": "Terminate apps"
}
}
},
"exceptions": {
"command_error": {
"message": "Error sending command to {device}: {error}"
},
"command_error_not_found": {
"message": "Error sending command to {device}: the Bravia API service is reloading"
},
"command_error_offline": {
"message": "Error sending command to {device}: the TV is turned off"
},
"update_error": {
"message": "Error updating data for {device}: {error}"
},
"update_error_not_found": {
"message": "Error updating data for {device}: the Bravia API service is stuck"
}
}
}

View File

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

View File

@@ -33,7 +33,7 @@ HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_HVAC_MODE): vol.All(
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
cv.ensure_list, vol.Length(min=1), [HVACMode]
),
},
}

View File

@@ -19,10 +19,6 @@
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
@@ -31,11 +27,14 @@
- input_number
- number
- sensor
number:
selector:
number:
mode: box
translation_key: number_or_entity
.trigger_threshold_type: &trigger_threshold_type
required: true
default: above
selector:
select:
options:

View File

@@ -28,11 +28,10 @@ async def async_setup_entry(
DemoHumidifier(
name="Humidifier",
mode=None,
target_humidity=65,
target_humidity=68,
current_humidity=45,
action=HumidifierAction.HUMIDIFYING,
device_class=HumidifierDeviceClass.HUMIDIFIER,
target_humidity_step=5,
),
DemoHumidifier(
name="Dehumidifier",
@@ -67,7 +66,6 @@ class DemoHumidifier(HumidifierEntity):
is_on: bool = True,
action: HumidifierAction | None = None,
device_class: HumidifierDeviceClass | None = None,
target_humidity_step: float | None = None,
) -> None:
"""Initialize the humidifier device."""
self._attr_name = name
@@ -81,7 +79,6 @@ class DemoHumidifier(HumidifierEntity):
self._attr_mode = mode
self._attr_available_modes = available_modes
self._attr_device_class = device_class
self._attr_target_humidity_step = target_humidity_step
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.1"]
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
}

View File

@@ -37,7 +37,7 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
name=device.name,
connections={(CONNECTION_NETWORK_MAC, device.mac_address)},
manufacturer="EHEIM",
model=device.model_name,
model=device.device_type.model_name,
identifiers={(DOMAIN, device.mac_address)},
suggested_area=device.aquarium_name,
sw_version=device.sw_version,
@@ -59,9 +59,9 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
def exception_handler[_EntityT: EheimDigitalEntity[EheimDigitalDevice], **_P](
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
"""Decorate eheimdigital calls to handle exceptions.
"""Decorate AirGradient calls to handle exceptions.
A decorator that wraps the passed in function, catches eheimdigital errors.
A decorator that wraps the passed in function, catches AirGradient errors.
"""
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:

View File

@@ -6,7 +6,6 @@ from typing import Any, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.filter import EheimDigitalFilter
from eheimdigital.heater import EheimDigitalHeater
from eheimdigital.types import HeaterUnit
@@ -22,7 +21,6 @@ from homeassistant.const import (
PRECISION_WHOLE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -44,34 +42,6 @@ class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice](
uom_fn: Callable[[_DeviceT], str] | None = None
FILTER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalFilter], ...] = (
EheimDigitalNumberDescription[EheimDigitalFilter](
key="high_pulse_time",
translation_key="high_pulse_time",
entity_category=EntityCategory.CONFIG,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=NumberDeviceClass.DURATION,
native_min_value=5,
native_max_value=200000,
value_fn=lambda device: device.high_pulse_time,
set_value_fn=lambda device, value: device.set_high_pulse_time(int(value)),
),
EheimDigitalNumberDescription[EheimDigitalFilter](
key="low_pulse_time",
translation_key="low_pulse_time",
entity_category=EntityCategory.CONFIG,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=NumberDeviceClass.DURATION,
native_min_value=5,
native_max_value=200000,
value_fn=lambda device: device.low_pulse_time,
set_value_fn=lambda device, value: device.set_low_pulse_time(int(value)),
),
)
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalNumberDescription[EheimDigitalClassicVario], ...
] = (
@@ -175,13 +145,6 @@ async def async_setup_entry(
)
for description in CLASSICVARIO_DESCRIPTIONS
)
if isinstance(device, EheimDigitalFilter):
entities.extend(
EheimDigitalNumber[EheimDigitalFilter](
coordinator, device, description
)
for description in FILTER_DESCRIPTIONS
)
if isinstance(device, EheimDigitalHeater):
entities.extend(
EheimDigitalNumber[EheimDigitalHeater](

View File

@@ -2,19 +2,13 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, Literal, override
from typing import Any, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.filter import EheimDigitalFilter
from eheimdigital.types import (
FilterMode,
FilterModeProf,
UnitOfMeasurement as EheimDigitalUnitOfMeasurement,
)
from eheimdigital.types import FilterMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory, UnitOfFrequency, UnitOfVolumeFlowRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -30,109 +24,8 @@ class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice](
):
"""Class describing EHEIM Digital select entities."""
options_fn: Callable[[_DeviceT], list[str]] | None = None
use_api_unit: Literal[True] | None = None
value_fn: Callable[[_DeviceT], str | None]
set_value_fn: Callable[[_DeviceT, str], Awaitable[None] | None]
FILTER_DESCRIPTIONS: tuple[EheimDigitalSelectDescription[EheimDigitalFilter], ...] = (
EheimDigitalSelectDescription[EheimDigitalFilter](
key="filter_mode",
translation_key="filter_mode",
entity_category=EntityCategory.CONFIG,
options=[item.lower() for item in FilterModeProf._member_names_],
value_fn=lambda device: device.filter_mode.name.lower(),
set_value_fn=lambda device, value: device.set_filter_mode(
FilterModeProf[value.upper()]
),
),
EheimDigitalSelectDescription[EheimDigitalFilter](
key="manual_speed",
translation_key="manual_speed",
entity_category=EntityCategory.CONFIG,
unit_of_measurement=UnitOfFrequency.HERTZ,
options_fn=lambda device: [str(i) for i in device.filter_manual_values],
value_fn=lambda device: str(device.manual_speed),
set_value_fn=lambda device, value: device.set_manual_speed(float(value)),
),
EheimDigitalSelectDescription[EheimDigitalFilter](
key="const_flow_speed",
translation_key="const_flow_speed",
entity_category=EntityCategory.CONFIG,
use_api_unit=True,
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
value_fn=lambda device: str(device.filter_const_flow_values[device.const_flow]),
set_value_fn=(
lambda device, value: device.set_const_flow(
device.filter_const_flow_values.index(int(value))
)
),
),
EheimDigitalSelectDescription[EheimDigitalFilter](
key="day_speed",
translation_key="day_speed",
entity_category=EntityCategory.CONFIG,
use_api_unit=True,
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
value_fn=lambda device: str(device.filter_const_flow_values[device.day_speed]),
set_value_fn=(
lambda device, value: device.set_day_speed(
device.filter_const_flow_values.index(int(value))
)
),
),
EheimDigitalSelectDescription[EheimDigitalFilter](
key="night_speed",
translation_key="night_speed",
entity_category=EntityCategory.CONFIG,
use_api_unit=True,
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
value_fn=lambda device: str(
device.filter_const_flow_values[device.night_speed]
),
set_value_fn=(
lambda device, value: device.set_night_speed(
device.filter_const_flow_values.index(int(value))
)
),
),
EheimDigitalSelectDescription[EheimDigitalFilter](
key="high_pulse_speed",
translation_key="high_pulse_speed",
entity_category=EntityCategory.CONFIG,
use_api_unit=True,
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
value_fn=lambda device: str(
device.filter_const_flow_values[device.high_pulse_speed]
),
set_value_fn=(
lambda device, value: device.set_high_pulse_speed(
device.filter_const_flow_values.index(int(value))
)
),
),
EheimDigitalSelectDescription[EheimDigitalFilter](
key="low_pulse_speed",
translation_key="low_pulse_speed",
entity_category=EntityCategory.CONFIG,
use_api_unit=True,
unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
options_fn=lambda device: [str(i) for i in device.filter_const_flow_values],
value_fn=lambda device: str(
device.filter_const_flow_values[device.low_pulse_speed]
),
set_value_fn=(
lambda device, value: device.set_low_pulse_speed(
device.filter_const_flow_values.index(int(value))
)
),
),
)
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
CLASSICVARIO_DESCRIPTIONS: tuple[
@@ -141,7 +34,11 @@ CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSelectDescription[EheimDigitalClassicVario](
key="filter_mode",
translation_key="filter_mode",
value_fn=lambda device: device.filter_mode.name.lower(),
value_fn=(
lambda device: device.filter_mode.name.lower()
if device.filter_mode is not None
else None
),
set_value_fn=(
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
),
@@ -171,11 +68,6 @@ async def async_setup_entry(
)
for description in CLASSICVARIO_DESCRIPTIONS
)
if isinstance(device, EheimDigitalFilter):
entities.extend(
EheimDigitalFilterSelect(coordinator, device, description)
for description in FILTER_DESCRIPTIONS
)
async_add_entities(entities)
@@ -190,8 +82,6 @@ class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
entity_description: EheimDigitalSelectDescription[_DeviceT]
_attr_options: list[str]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
@@ -201,49 +91,13 @@ class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
"""Initialize an EHEIM Digital select entity."""
super().__init__(coordinator, device)
self.entity_description = description
if description.options_fn is not None:
self._attr_options = description.options_fn(device)
elif description.options is not None:
self._attr_options = description.options
self._attr_unique_id = f"{self._device_address}_{description.key}"
@override
@exception_handler
async def async_select_option(self, option: str) -> None:
if await_return := self.entity_description.set_value_fn(self._device, option):
return await await_return
return None
return await self.entity_description.set_value_fn(self._device, option)
@override
def _async_update_attrs(self) -> None:
self._attr_current_option = self.entity_description.value_fn(self._device)
class EheimDigitalFilterSelect(EheimDigitalSelect[EheimDigitalFilter]):
"""Represent an EHEIM Digital Filter select entity."""
entity_description: EheimDigitalSelectDescription[EheimDigitalFilter]
_attr_native_unit_of_measurement: str | None
@override
def _async_update_attrs(self) -> None:
if (
self.entity_description.options is None
and self.entity_description.options_fn is not None
):
self._attr_options = self.entity_description.options_fn(self._device)
if self.entity_description.use_api_unit:
if (
self.entity_description.unit_of_measurement
== UnitOfVolumeFlowRate.LITERS_PER_HOUR
and self._device.usrdta["unit"]
== int(EheimDigitalUnitOfMeasurement.US_CUSTOMARY)
):
self._attr_native_unit_of_measurement = (
UnitOfVolumeFlowRate.GALLONS_PER_HOUR
)
else:
self._attr_native_unit_of_measurement = (
self.entity_description.unit_of_measurement
)
super()._async_update_attrs()

View File

@@ -6,7 +6,6 @@ from typing import Any, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.filter import EheimDigitalFilter
from eheimdigital.types import FilterErrorCode
from homeassistant.components.sensor import (
@@ -14,7 +13,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfFrequency, UnitOfTime
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -34,27 +33,6 @@ class EheimDigitalSensorDescription[_DeviceT: EheimDigitalDevice](
value_fn: Callable[[_DeviceT], float | str | None]
FILTER_DESCRIPTIONS: tuple[EheimDigitalSensorDescription[EheimDigitalFilter], ...] = (
EheimDigitalSensorDescription[EheimDigitalFilter](
key="current_speed",
translation_key="current_speed",
value_fn=lambda device: device.current_speed,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=1,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
),
EheimDigitalSensorDescription[EheimDigitalFilter](
key="service_hours",
translation_key="service_hours",
value_fn=lambda device: device.service_hours,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
suggested_unit_of_measurement=UnitOfTime.DAYS,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSensorDescription[EheimDigitalClassicVario], ...
] = (
@@ -76,7 +54,11 @@ CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSensorDescription[EheimDigitalClassicVario](
key="error_code",
translation_key="error_code",
value_fn=lambda device: device.error_code.name.lower(),
value_fn=(
lambda device: device.error_code.name.lower()
if device.error_code is not None
else None
),
device_class=SensorDeviceClass.ENUM,
options=[name.lower() for name in FilterErrorCode._member_names_],
entity_category=EntityCategory.DIAGNOSTIC,
@@ -98,13 +80,6 @@ async def async_setup_entry(
"""Set up the light entities for one or multiple devices."""
entities: list[EheimDigitalSensor[Any]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalFilter):
entities += [
EheimDigitalSensor[EheimDigitalFilter](
coordinator, device, description
)
for description in FILTER_DESCRIPTIONS
]
if isinstance(device, EheimDigitalClassicVario):
entities += [
EheimDigitalSensor[EheimDigitalClassicVario](

View File

@@ -61,12 +61,6 @@
"day_speed": {
"name": "Day speed"
},
"high_pulse_time": {
"name": "High pulse duration"
},
"low_pulse_time": {
"name": "Low pulse duration"
},
"manual_speed": {
"name": "Manual speed"
},
@@ -84,32 +78,13 @@
}
},
"select": {
"const_flow_speed": {
"name": "Constant flow speed"
},
"day_speed": {
"name": "Day speed"
},
"filter_mode": {
"name": "Filter mode",
"state": {
"bio": "Bio",
"constant_flow": "Constant flow",
"manual": "Manual",
"pulse": "Pulse"
}
},
"high_pulse_speed": {
"name": "High pulse speed"
},
"low_pulse_speed": {
"name": "Low pulse speed"
},
"manual_speed": {
"name": "Manual speed"
},
"night_speed": {
"name": "Night speed"
}
},
"sensor": {
@@ -124,17 +99,8 @@
"rotor_stuck": "Rotor stuck"
}
},
"operating_time": {
"name": "Operating time"
},
"service_hours": {
"name": "Remaining hours until service"
},
"turn_feeding_time": {
"name": "Remaining off time after feeding"
},
"turn_off_time": {
"name": "Remaining off time"
}
},
"time": {

View File

@@ -4,7 +4,6 @@ from typing import Any, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.filter import EheimDigitalFilter
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
@@ -31,8 +30,8 @@ async def async_setup_entry(
"""Set up the switch entities for one or multiple devices."""
entities: list[SwitchEntity] = []
for device in device_address.values():
if isinstance(device, (EheimDigitalClassicVario, EheimDigitalFilter)):
entities.append(EheimDigitalFilterSwitch(coordinator, device)) # noqa: PERF401
if isinstance(device, EheimDigitalClassicVario):
entities.append(EheimDigitalClassicVarioSwitch(coordinator, device)) # noqa: PERF401
async_add_entities(entities)
@@ -40,10 +39,10 @@ async def async_setup_entry(
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalFilterSwitch(
EheimDigitalEntity[EheimDigitalClassicVario | EheimDigitalFilter], SwitchEntity
class EheimDigitalClassicVarioSwitch(
EheimDigitalEntity[EheimDigitalClassicVario], SwitchEntity
):
"""Represent an EHEIM Digital classicVARIO or filter switch entity."""
"""Represent an EHEIM Digital classicVARIO switch entity."""
_attr_translation_key = "filter_active"
_attr_name = None
@@ -51,9 +50,9 @@ class EheimDigitalFilterSwitch(
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: EheimDigitalClassicVario | EheimDigitalFilter,
device: EheimDigitalClassicVario,
) -> None:
"""Initialize an EHEIM Digital classicVARIO or filter switch entity."""
"""Initialize an EHEIM Digital classicVARIO switch entity."""
super().__init__(coordinator, device)
self._attr_unique_id = device.mac_address
self._async_update_attrs()

View File

@@ -7,7 +7,6 @@ from typing import Any, final, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.filter import EheimDigitalFilter
from eheimdigital.heater import EheimDigitalHeater
from homeassistant.components.time import TimeEntity, TimeEntityDescription
@@ -29,23 +28,6 @@ class EheimDigitalTimeDescription[_DeviceT: EheimDigitalDevice](TimeEntityDescri
set_value_fn: Callable[[_DeviceT, time], Awaitable[None]]
FILTER_DESCRIPTIONS: tuple[EheimDigitalTimeDescription[EheimDigitalFilter], ...] = (
EheimDigitalTimeDescription[EheimDigitalFilter](
key="day_start_time",
translation_key="day_start_time",
entity_category=EntityCategory.CONFIG,
value_fn=lambda device: device.day_start_time,
set_value_fn=lambda device, value: device.set_day_start_time(value),
),
EheimDigitalTimeDescription[EheimDigitalFilter](
key="night_start_time",
translation_key="night_start_time",
entity_category=EntityCategory.CONFIG,
value_fn=lambda device: device.night_start_time,
set_value_fn=lambda device, value: device.set_night_start_time(value),
),
)
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalTimeDescription[EheimDigitalClassicVario], ...
] = (
@@ -97,13 +79,6 @@ async def async_setup_entry(
"""Set up the time entities for one or multiple devices."""
entities: list[EheimDigitalTime[Any]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalFilter):
entities.extend(
EheimDigitalTime[EheimDigitalFilter](
coordinator, device, description
)
for description in FILTER_DESCRIPTIONS
)
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
EheimDigitalTime[EheimDigitalClassicVario](

View File

@@ -206,7 +206,7 @@ class EnvoyProductionSensorEntityDescription(SensorEntityDescription):
"""Describes an Envoy production sensor entity."""
value_fn: Callable[[EnvoySystemProduction], int]
on_phase: str | None = None
on_phase: str | None
PRODUCTION_SENSORS = (
@@ -219,6 +219,7 @@ PRODUCTION_SENSORS = (
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
suggested_display_precision=3,
value_fn=attrgetter("watts_now"),
on_phase=None,
),
EnvoyProductionSensorEntityDescription(
key="daily_production",
@@ -229,6 +230,7 @@ PRODUCTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
value_fn=attrgetter("watt_hours_today"),
on_phase=None,
),
EnvoyProductionSensorEntityDescription(
key="seven_days_production",
@@ -238,6 +240,7 @@ PRODUCTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=1,
value_fn=attrgetter("watt_hours_last_7_days"),
on_phase=None,
),
EnvoyProductionSensorEntityDescription(
key="lifetime_production",
@@ -248,6 +251,7 @@ PRODUCTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("watt_hours_lifetime"),
on_phase=None,
),
)
@@ -273,7 +277,7 @@ class EnvoyConsumptionSensorEntityDescription(SensorEntityDescription):
"""Describes an Envoy consumption sensor entity."""
value_fn: Callable[[EnvoySystemConsumption], int]
on_phase: str | None = None
on_phase: str | None
CONSUMPTION_SENSORS = (
@@ -286,6 +290,7 @@ CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
suggested_display_precision=3,
value_fn=attrgetter("watts_now"),
on_phase=None,
),
EnvoyConsumptionSensorEntityDescription(
key="daily_consumption",
@@ -296,6 +301,7 @@ CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
value_fn=attrgetter("watt_hours_today"),
on_phase=None,
),
EnvoyConsumptionSensorEntityDescription(
key="seven_days_consumption",
@@ -305,6 +311,7 @@ CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=1,
value_fn=attrgetter("watt_hours_last_7_days"),
on_phase=None,
),
EnvoyConsumptionSensorEntityDescription(
key="lifetime_consumption",
@@ -315,6 +322,7 @@ CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("watt_hours_lifetime"),
on_phase=None,
),
)
@@ -346,6 +354,7 @@ NET_CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
suggested_display_precision=3,
value_fn=attrgetter("watts_now"),
on_phase=None,
),
EnvoyConsumptionSensorEntityDescription(
key="lifetime_balanced_net_consumption",
@@ -357,6 +366,7 @@ NET_CONSUMPTION_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("watt_hours_lifetime"),
on_phase=None,
),
)
@@ -385,7 +395,7 @@ class EnvoyCTSensorEntityDescription(SensorEntityDescription):
[EnvoyMeterData],
int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None,
]
on_phase: str | None = None
on_phase: str | None
cttype: str | None = None
@@ -401,6 +411,7 @@ CT_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("energy_delivered"),
on_phase=None,
cttype=cttype,
)
for cttype, key in (
@@ -419,6 +430,7 @@ CT_SENSORS = (
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
suggested_display_precision=3,
value_fn=attrgetter("energy_received"),
on_phase=None,
cttype=cttype,
)
for cttype, key in (
@@ -437,6 +449,7 @@ CT_SENSORS = (
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
suggested_display_precision=3,
value_fn=attrgetter("active_power"),
on_phase=None,
cttype=cttype,
)
for cttype, key in (
@@ -455,6 +468,7 @@ CT_SENSORS = (
suggested_display_precision=1,
entity_registry_enabled_default=False,
value_fn=attrgetter("frequency"),
on_phase=None,
cttype=cttype,
)
for cttype, key, translation_key in (
@@ -474,6 +488,7 @@ CT_SENSORS = (
suggested_display_precision=1,
entity_registry_enabled_default=False,
value_fn=attrgetter("voltage"),
on_phase=None,
cttype=cttype,
)
for cttype, key, translation_key in (
@@ -493,6 +508,7 @@ CT_SENSORS = (
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("current"),
on_phase=None,
cttype=cttype,
)
for cttype, key in (
@@ -510,6 +526,7 @@ CT_SENSORS = (
suggested_display_precision=2,
entity_registry_enabled_default=False,
value_fn=attrgetter("power_factor"),
on_phase=None,
cttype=cttype,
)
for cttype, key in (
@@ -527,6 +544,7 @@ CT_SENSORS = (
options=list(CtMeterStatus),
entity_registry_enabled_default=False,
value_fn=attrgetter("metering_status"),
on_phase=None,
cttype=cttype,
)
for cttype, key, translation_key in (
@@ -547,6 +565,7 @@ CT_SENSORS = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None,
cttype=cttype,
)
for cttype, key, translation_key in (
@@ -764,7 +783,7 @@ ENCHARGE_AGGREGATE_SENSORS = (
translation_key="available_energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.ENERGY_STORAGE,
device_class=SensorDeviceClass.ENERGY,
value_fn=attrgetter("available_energy"),
),
EnvoyEnchargeAggregateSensorEntityDescription(
@@ -772,14 +791,14 @@ ENCHARGE_AGGREGATE_SENSORS = (
translation_key="reserve_energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.ENERGY_STORAGE,
device_class=SensorDeviceClass.ENERGY,
value_fn=attrgetter("backup_reserve"),
),
EnvoyEnchargeAggregateSensorEntityDescription(
key="max_capacity",
translation_key="max_capacity",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY_STORAGE,
device_class=SensorDeviceClass.ENERGY,
value_fn=attrgetter("max_available_capacity"),
),
)

View File

@@ -77,14 +77,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
)
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
try:
self.has_triggers = await self.hass.async_add_executor_job(
self.fritz.has_triggers
)
except HTTPError:
# Fritz!OS < 7.39 just don't have this api endpoint
# so we need to fetch the HTTPError here and assume no triggers
self.has_triggers = False
self.has_triggers = await self.hass.async_add_executor_job(
self.fritz.has_triggers
)
LOGGER.debug("enable smarthome triggers: %s", self.has_triggers)
self.configuration_url = self.fritz.get_prefixed_host()

View File

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

View File

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

View File

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

View File

@@ -1,31 +0,0 @@
"""The HDFury Integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
PLATFORMS = [
Platform.BUTTON,
Platform.SELECT,
Platform.SWITCH,
]
async def async_setup_entry(hass: HomeAssistant, entry: HDFuryConfigEntry) -> bool:
"""Set up HDFury as config entry."""
coordinator = HDFuryCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HDFuryConfigEntry) -> bool:
"""Unload a HDFury config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,74 +0,0 @@
"""Button platform for HDFury Integration."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from hdfury import HDFuryAPI, HDFuryError
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
@dataclass(kw_only=True, frozen=True)
class HDFuryButtonEntityDescription(ButtonEntityDescription):
"""Description for HDFury button entities."""
press_fn: Callable[[HDFuryAPI], Awaitable[None]]
BUTTONS: tuple[HDFuryButtonEntityDescription, ...] = (
HDFuryButtonEntityDescription(
key="reboot",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
press_fn=lambda client: client.issue_reboot(),
),
HDFuryButtonEntityDescription(
key="issue_hotplug",
translation_key="issue_hotplug",
entity_category=EntityCategory.CONFIG,
press_fn=lambda client: client.issue_hotplug(),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: HDFuryConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up buttons using the platform schema."""
coordinator = entry.runtime_data
async_add_entities(
HDFuryButton(coordinator, description) for description in BUTTONS
)
class HDFuryButton(HDFuryEntity, ButtonEntity):
"""HDFury Button Class."""
entity_description: HDFuryButtonEntityDescription
async def async_press(self) -> None:
"""Handle Button Press."""
try:
await self.entity_description.press_fn(self.coordinator.client)
except HDFuryError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error

View File

@@ -1,54 +0,0 @@
"""Config flow for HDFury Integration."""
from typing import Any
from hdfury import HDFuryAPI, HDFuryError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
class HDFuryConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle Config Flow for HDFury."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle Initial Setup."""
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
serial = await self._validate_connection(host)
if serial is not None:
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"HDFury ({host})", data=user_input
)
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=errors,
)
async def _validate_connection(self, host: str) -> str | None:
"""Try to fetch serial number to confirm it's a valid HDFury device."""
client = HDFuryAPI(host, async_get_clientsession(self.hass))
try:
data = await client.get_board()
except HDFuryError:
return None
return data["serial"]

View File

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

View File

@@ -1,67 +0,0 @@
"""DataUpdateCoordinator for HDFury Integration."""
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Final
from hdfury import HDFuryAPI, HDFuryError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL: Final = timedelta(seconds=60)
type HDFuryConfigEntry = ConfigEntry[HDFuryCoordinator]
@dataclass(kw_only=True, frozen=True)
class HDFuryData:
"""HDFury Data Class."""
board: dict[str, str]
info: dict[str, str]
config: dict[str, str]
class HDFuryCoordinator(DataUpdateCoordinator[HDFuryData]):
"""HDFury Device Coordinator Class."""
def __init__(self, hass: HomeAssistant, entry: HDFuryConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name="HDFury",
update_interval=SCAN_INTERVAL,
)
self.host: str = entry.data[CONF_HOST]
self.client = HDFuryAPI(self.host, async_get_clientsession(hass))
async def _async_update_data(self) -> HDFuryData:
"""Fetch the latest device data."""
try:
board = await self.client.get_board()
info = await self.client.get_info()
config = await self.client.get_config()
except HDFuryError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
return HDFuryData(
board=board,
info=info,
config=config,
)

View File

@@ -1,39 +0,0 @@
"""Base class for HDFury entities."""
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import HDFuryCoordinator
class HDFuryEntity(CoordinatorEntity[HDFuryCoordinator]):
"""Common elements for all entities."""
_attr_has_entity_name = True
def __init__(
self, coordinator: HDFuryCoordinator, entity_description: EntityDescription
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = (
f"{coordinator.data.board['serial']}_{entity_description.key}"
)
self._attr_device_info = DeviceInfo(
name=f"HDFury {coordinator.data.board['hostname']}",
manufacturer="HDFury",
model=coordinator.data.board["hostname"].split("-")[0],
serial_number=coordinator.data.board["serial"],
sw_version=coordinator.data.board["version"].removeprefix("FW: "),
hw_version=coordinator.data.board.get("pcbv"),
configuration_url=f"http://{coordinator.host}",
connections={
(dr.CONNECTION_NETWORK_MAC, coordinator.data.config["macaddr"])
},
)

View File

@@ -1,52 +0,0 @@
{
"entity": {
"button": {
"issue_hotplug": {
"default": "mdi:connection"
}
},
"select": {
"opmode": {
"default": "mdi:cogs"
},
"portseltx0": {
"default": "mdi:hdmi-port"
},
"portseltx1": {
"default": "mdi:hdmi-port"
}
},
"switch": {
"autosw": {
"default": "mdi:import"
},
"htpcmode0": {
"default": "mdi:desktop-classic"
},
"htpcmode1": {
"default": "mdi:desktop-classic"
},
"htpcmode2": {
"default": "mdi:desktop-classic"
},
"htpcmode3": {
"default": "mdi:desktop-classic"
},
"iractive": {
"default": "mdi:remote"
},
"mutetx0": {
"default": "mdi:volume-mute"
},
"mutetx1": {
"default": "mdi:volume-mute"
},
"oled": {
"default": "mdi:cellphone-information"
},
"relay": {
"default": "mdi:electric-switch"
}
}
}
}

View File

@@ -1,11 +0,0 @@
{
"domain": "hdfury",
"name": "HDFury",
"codeowners": ["@glenndehaan"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hdfury",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["hdfury==1.3.1"]
}

View File

@@ -1,74 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Entities do not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: Integration has no options flow.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: Integration has no authentication flow.
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: Device type integration.
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt
comment: Device type integration.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -1,122 +0,0 @@
"""Select platform for HDFury Integration."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from hdfury import (
OPERATION_MODES,
TX0_INPUT_PORTS,
TX1_INPUT_PORTS,
HDFuryAPI,
HDFuryError,
)
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
from .entity import HDFuryEntity
@dataclass(kw_only=True, frozen=True)
class HDFurySelectEntityDescription(SelectEntityDescription):
"""Description for HDFury select entities."""
set_value_fn: Callable[[HDFuryAPI, str], Awaitable[None]]
SELECT_PORTS: tuple[HDFurySelectEntityDescription, ...] = (
HDFurySelectEntityDescription(
key="portseltx0",
translation_key="portseltx0",
options=list(TX0_INPUT_PORTS.keys()),
set_value_fn=lambda coordinator, value: _set_ports(coordinator),
),
HDFurySelectEntityDescription(
key="portseltx1",
translation_key="portseltx1",
options=list(TX1_INPUT_PORTS.keys()),
set_value_fn=lambda coordinator, value: _set_ports(coordinator),
),
)
SELECT_OPERATION_MODE: HDFurySelectEntityDescription = HDFurySelectEntityDescription(
key="opmode",
translation_key="opmode",
options=list(OPERATION_MODES.keys()),
set_value_fn=lambda coordinator, value: coordinator.client.set_operation_mode(
value
),
)
async def _set_ports(coordinator: HDFuryCoordinator) -> None:
tx0 = coordinator.data.info.get("portseltx0")
tx1 = coordinator.data.info.get("portseltx1")
if tx0 is None or tx1 is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="tx_state_error",
translation_placeholders={"details": f"tx0={tx0}, tx1={tx1}"},
)
await coordinator.client.set_port_selection(tx0, tx1)
async def async_setup_entry(
hass: HomeAssistant,
entry: HDFuryConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up selects using the platform schema."""
coordinator = entry.runtime_data
entities: list[HDFuryEntity] = []
for description in SELECT_PORTS:
if description.key not in coordinator.data.info:
continue
entities.append(HDFurySelect(coordinator, description))
# Add OPMODE select if present
if "opmode" in coordinator.data.info:
entities.append(HDFurySelect(coordinator, SELECT_OPERATION_MODE))
async_add_entities(entities)
class HDFurySelect(HDFuryEntity, SelectEntity):
"""HDFury Select Class."""
entity_description: HDFurySelectEntityDescription
@property
def current_option(self) -> str:
"""Return the current option."""
return self.coordinator.data.info[self.entity_description.key]
async def async_select_option(self, option: str) -> None:
"""Update the current option."""
# Update local data first
self.coordinator.data.info[self.entity_description.key] = option
# Send command to device
try:
await self.entity_description.set_value_fn(self.coordinator, option)
except HDFuryError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
# Trigger HA coordinator refresh
await self.coordinator.async_request_refresh()

View File

@@ -1,101 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "Hostname or IP address of your HDFury device."
},
"description": "Set up your HDFury to integrate with Home Assistant."
}
}
},
"entity": {
"button": {
"issue_hotplug": {
"name": "Issue hotplug"
}
},
"select": {
"opmode": {
"name": "Operation mode",
"state": {
"0": "Mode 0 - Splitter TX0/TX1 FRL5 VRR",
"1": "Mode 1 - Splitter TX0/TX1 UPSCALE FRL5",
"2": "Mode 2 - Matrix TMDS",
"3": "Mode 3 - Matrix FRL->TMDS",
"4": "Mode 4 - Matrix DOWNSCALE",
"5": "Mode 5 - Matrix RX0:FRL5 + RX1-3:TMDS"
}
},
"portseltx0": {
"name": "Port select TX0",
"state": {
"0": "Input 0",
"1": "Input 1",
"2": "Input 2",
"3": "Input 3",
"4": "Copy TX1"
}
},
"portseltx1": {
"name": "Port select TX1",
"state": {
"0": "Input 0",
"1": "Input 1",
"2": "Input 2",
"3": "Input 3",
"4": "Copy TX0"
}
}
},
"switch": {
"autosw": {
"name": "Auto switch inputs"
},
"htpcmode0": {
"name": "HTPC mode RX0"
},
"htpcmode1": {
"name": "HTPC mode RX1"
},
"htpcmode2": {
"name": "HTPC mode RX2"
},
"htpcmode3": {
"name": "HTPC mode RX3"
},
"iractive": {
"name": "Infrared"
},
"mutetx0": {
"name": "Mute audio TX0"
},
"mutetx1": {
"name": "Mute audio TX1"
},
"oled": {
"name": "OLED display"
},
"relay": {
"name": "Relay"
}
}
},
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with HDFury device"
},
"tx_state_error": {
"message": "An error occurred while validating TX states: {details}"
}
}
}

View File

@@ -1,142 +0,0 @@
"""Switch platform for HDFury Integration."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from hdfury import HDFuryAPI, HDFuryError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
@dataclass(kw_only=True, frozen=True)
class HDFurySwitchEntityDescription(SwitchEntityDescription):
"""Description for HDFury switch entities."""
set_value_fn: Callable[[HDFuryAPI, str], Awaitable[None]]
SWITCHES: tuple[HDFurySwitchEntityDescription, ...] = (
HDFurySwitchEntityDescription(
key="autosw",
translation_key="autosw",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_auto_switch_inputs(value),
),
HDFurySwitchEntityDescription(
key="htpcmode0",
translation_key="htpcmode0",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_htpc_mode_rx0(value),
),
HDFurySwitchEntityDescription(
key="htpcmode1",
translation_key="htpcmode1",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_htpc_mode_rx1(value),
),
HDFurySwitchEntityDescription(
key="htpcmode2",
translation_key="htpcmode2",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_htpc_mode_rx2(value),
),
HDFurySwitchEntityDescription(
key="htpcmode3",
translation_key="htpcmode3",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_htpc_mode_rx3(value),
),
HDFurySwitchEntityDescription(
key="mutetx0",
translation_key="mutetx0",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_mute_tx0_audio(value),
),
HDFurySwitchEntityDescription(
key="mutetx1",
translation_key="mutetx1",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_mute_tx1_audio(value),
),
HDFurySwitchEntityDescription(
key="oled",
translation_key="oled",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_oled(value),
),
HDFurySwitchEntityDescription(
key="iractive",
translation_key="iractive",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_ir_active(value),
),
HDFurySwitchEntityDescription(
key="relay",
translation_key="relay",
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_relay(value),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: HDFuryConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up switches using the platform schema."""
coordinator = entry.runtime_data
async_add_entities(
HDFurySwitch(coordinator, description)
for description in SWITCHES
if description.key in coordinator.data.config
)
class HDFurySwitch(HDFuryEntity, SwitchEntity):
"""Base HDFury Switch Class."""
entity_description: HDFurySwitchEntityDescription
@property
def is_on(self) -> bool:
"""Set Switch State."""
return self.coordinator.data.config.get(self.entity_description.key) == "1"
async def async_turn_on(self, **kwargs: Any) -> None:
"""Handle Switch On Event."""
try:
await self.entity_description.set_value_fn(self.coordinator.client, "on")
except HDFuryError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Handle Switch Off Event."""
try:
await self.entity_description.set_value_fn(self.coordinator.client, "off")
except HDFuryError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
await self.coordinator.async_request_refresh()

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ set_program_and_options:
- active_program
- selected_program
program:
example: dishcare_dishwasher_program_auto_2
example: dishcare_dishwasher_program_auto2
selector:
select:
mode: dropdown
@@ -73,7 +73,6 @@ set_program_and_options:
- dishcare_dishwasher_program_intensiv_45
- dishcare_dishwasher_program_auto_half_load
- dishcare_dishwasher_program_intensiv_power
- dishcare_dishwasher_program_intensive_fixed_zone
- dishcare_dishwasher_program_magic_daily
- dishcare_dishwasher_program_super_60
- dishcare_dishwasher_program_kurz_60
@@ -122,7 +121,6 @@ set_program_and_options:
- cooking_oven_program_heating_mode_pre_heating
- cooking_oven_program_heating_mode_hot_air
- cooking_oven_program_heating_mode_hot_air_eco
- cooking_oven_program_heating_mode_hot_air_gentle
- cooking_oven_program_heating_mode_hot_air_grilling
- cooking_oven_program_heating_mode_top_bottom_heating
- cooking_oven_program_heating_mode_top_bottom_heating_eco
@@ -149,7 +147,6 @@ set_program_and_options:
- cooking_oven_program_microwave_900_watt
- cooking_oven_program_microwave_1000_watt
- cooking_oven_program_microwave_max
- cooking_oven_program_steam_modes_steam
- cooking_oven_program_heating_mode_warming_drawer
- laundry_care_washer_program_auto_30
- laundry_care_washer_program_auto_40
@@ -177,7 +174,7 @@ set_program_and_options:
- laundry_care_washer_program_rinse_rinse_spin_drain
- laundry_care_washer_program_sensitive
- laundry_care_washer_program_shirts_blouses
- laundry_care_washer_program_spin_spin_drain
- laundry_care_washer_program_spin_drain
- laundry_care_washer_program_sport_fitness
- laundry_care_washer_program_super_153045_super_15
- laundry_care_washer_program_super_153045_super_1530

View File

@@ -240,7 +240,6 @@
"cooking_oven_program_heating_mode_hot_air_60_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_60_steam%]",
"cooking_oven_program_heating_mode_hot_air_80_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_80_steam%]",
"cooking_oven_program_heating_mode_hot_air_eco": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_eco%]",
"cooking_oven_program_heating_mode_hot_air_gentle": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_gentle%]",
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_grilling%]",
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_intensive_heat%]",
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_keep_warm%]",
@@ -272,7 +271,6 @@
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
"dishcare_dishwasher_program_intensive_fixed_zone": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensive_fixed_zone%]",
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_kurz_60%]",
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
@@ -352,7 +350,7 @@
"laundry_care_washer_program_rinse_rinse_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_rinse_rinse_spin_drain%]",
"laundry_care_washer_program_sensitive": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sensitive%]",
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_shirts_blouses%]",
"laundry_care_washer_program_spin_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
"laundry_care_washer_program_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_drain%]",
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_fitness%]",
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_15%]",
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_1530%]",
@@ -594,7 +592,6 @@
"cooking_oven_program_heating_mode_hot_air_60_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_60_steam%]",
"cooking_oven_program_heating_mode_hot_air_80_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_80_steam%]",
"cooking_oven_program_heating_mode_hot_air_eco": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_eco%]",
"cooking_oven_program_heating_mode_hot_air_gentle": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_gentle%]",
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_grilling%]",
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_intensive_heat%]",
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_keep_warm%]",
@@ -615,7 +612,6 @@
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_900_watt%]",
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_90_watt%]",
"cooking_oven_program_microwave_max": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_max%]",
"cooking_oven_program_steam_modes_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_steam%]",
"dishcare_dishwasher_program_auto_1": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_1%]",
"dishcare_dishwasher_program_auto_2": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_2%]",
"dishcare_dishwasher_program_auto_3": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_3%]",
@@ -627,7 +623,6 @@
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
"dishcare_dishwasher_program_intensive_fixed_zone": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensive_fixed_zone%]",
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_kurz_60%]",
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
@@ -707,7 +702,7 @@
"laundry_care_washer_program_rinse_rinse_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_rinse_rinse_spin_drain%]",
"laundry_care_washer_program_sensitive": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sensitive%]",
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_shirts_blouses%]",
"laundry_care_washer_program_spin_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
"laundry_care_washer_program_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_drain%]",
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_fitness%]",
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_15%]",
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_1530%]",
@@ -1588,7 +1583,6 @@
"cooking_oven_program_heating_mode_hot_air_60_steam": "Hot air + 60 RH",
"cooking_oven_program_heating_mode_hot_air_80_steam": "Hot air + 80 RH",
"cooking_oven_program_heating_mode_hot_air_eco": "Hot air eco",
"cooking_oven_program_heating_mode_hot_air_gentle": "Hot air gentle",
"cooking_oven_program_heating_mode_hot_air_grilling": "Hot air grilling",
"cooking_oven_program_heating_mode_intensive_heat": "Intensive heat",
"cooking_oven_program_heating_mode_keep_warm": "Keep warm",
@@ -1609,7 +1603,6 @@
"cooking_oven_program_microwave_900_watt": "900 Watt",
"cooking_oven_program_microwave_90_watt": "90 Watt",
"cooking_oven_program_microwave_max": "Max",
"cooking_oven_program_steam_modes_steam": "Steam mode",
"dishcare_dishwasher_program_auto_1": "Auto 1",
"dishcare_dishwasher_program_auto_2": "Auto 2",
"dishcare_dishwasher_program_auto_3": "Auto 3",
@@ -1621,7 +1614,6 @@
"dishcare_dishwasher_program_intensiv_45": "Intensive 45ºC",
"dishcare_dishwasher_program_intensiv_70": "Intensive 70ºC",
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
"dishcare_dishwasher_program_intensive_fixed_zone": "Intensive fixed zone",
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
"dishcare_dishwasher_program_learning_dishwasher": "Intelligent",
"dishcare_dishwasher_program_machine_care": "Machine care",
@@ -1701,7 +1693,7 @@
"laundry_care_washer_program_rinse_rinse_spin_drain": "Rinse spin drain",
"laundry_care_washer_program_sensitive": "Sensitive",
"laundry_care_washer_program_shirts_blouses": "Shirts/blouses",
"laundry_care_washer_program_spin_spin_drain": "Spin/drain",
"laundry_care_washer_program_spin_drain": "Spin/drain",
"laundry_care_washer_program_sport_fitness": "Sport/fitness",
"laundry_care_washer_program_super_153045_super_15": "Super 15 min",
"laundry_care_washer_program_super_153045_super_1530": "Super 15/30 min",

View File

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

View File

@@ -34,7 +34,6 @@ from .const import ( # noqa: F401
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
ATTR_TARGET_HUMIDITY_STEP,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DOMAIN,
@@ -142,7 +141,6 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"min_humidity",
"max_humidity",
"supported_features",
"target_humidity_step",
}
@@ -150,12 +148,7 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
"""Base class for humidifier entities."""
_entity_component_unrecorded_attributes = frozenset(
{
ATTR_MIN_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_AVAILABLE_MODES,
ATTR_TARGET_HUMIDITY_STEP,
}
{ATTR_MIN_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_AVAILABLE_MODES}
)
entity_description: HumidifierEntityDescription
@@ -168,7 +161,6 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
_attr_mode: str | None
_attr_supported_features: HumidifierEntityFeature = HumidifierEntityFeature(0)
_attr_target_humidity: float | None = None
_attr_target_humidity_step: float | None = None
@property
def capability_attributes(self) -> dict[str, Any]:
@@ -177,8 +169,6 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
ATTR_MIN_HUMIDITY: self.min_humidity,
ATTR_MAX_HUMIDITY: self.max_humidity,
}
if self.target_humidity_step is not None:
data[ATTR_TARGET_HUMIDITY_STEP] = self.target_humidity_step
if HumidifierEntityFeature.MODES in self.supported_features:
data[ATTR_AVAILABLE_MODES] = self.available_modes
@@ -261,11 +251,6 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
"""Set new mode."""
await self.hass.async_add_executor_job(self.set_mode, mode)
@cached_property
def target_humidity_step(self) -> float | None:
"""Return the supported step of humidity."""
return self._attr_target_humidity_step
@cached_property
def min_humidity(self) -> float:
"""Return the minimum humidity."""

View File

@@ -28,7 +28,6 @@ ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_TARGET_HUMIDITY_STEP = "target_humidity_step"
DEFAULT_MIN_HUMIDITY = 0
DEFAULT_MAX_HUMIDITY = 100

View File

@@ -19,10 +19,6 @@
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
@@ -31,11 +27,14 @@
- input_number
- number
- sensor
number:
selector:
number:
mode: box
translation_key: number_or_entity
.trigger_threshold_type: &trigger_threshold_type
required: true
default: above
selector:
select:
options:

View File

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

View File

@@ -116,8 +116,6 @@ class IsraelRailEntitySensor(
@property
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
if self.entity_description.index >= len(self.coordinator.data):
return None
return self.entity_description.value_fn(
self.coordinator.data[self.entity_description.index]
)

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==1.1.3"]
"requirements": ["pyjvcprojector==1.1.2"]
}

View File

@@ -41,13 +41,6 @@ COMMANDS = {
"mode_1": const.REMOTE_MODE_1,
"mode_2": const.REMOTE_MODE_2,
"mode_3": const.REMOTE_MODE_3,
"mode_4": const.REMOTE_MODE_4,
"mode_5": const.REMOTE_MODE_5,
"mode_6": const.REMOTE_MODE_6,
"mode_7": const.REMOTE_MODE_7,
"mode_8": const.REMOTE_MODE_8,
"mode_9": const.REMOTE_MODE_9,
"mode_10": const.REMOTE_MODE_10,
"lens_ap": const.REMOTE_LENS_AP,
"gamma": const.REMOTE_GAMMA,
"color_temp": const.REMOTE_COLOR_TEMP,

View File

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

View File

@@ -1,195 +0,0 @@
"""Support for humidifier entities."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import Any
from thinqconnect import DeviceType
from thinqconnect.devices.const import Property as ThinQProperty
from thinqconnect.integration import ActiveMode
from homeassistant.components.humidifier import (
HumidifierAction,
HumidifierDeviceClass,
HumidifierEntity,
HumidifierEntityDescription,
HumidifierEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ThinqConfigEntry
from .coordinator import DeviceDataUpdateCoordinator
from .entity import ThinQEntity
@dataclass(frozen=True, kw_only=True)
class ThinQHumidifierEntityDescription(HumidifierEntityDescription):
"""Describes ThinQ humidifier entity."""
current_humidity_key: str
operation_key: str
mode_key: str = ThinQProperty.CURRENT_JOB_MODE
DEVICE_TYPE_HUM_MAP: dict[DeviceType, ThinQHumidifierEntityDescription] = {
DeviceType.DEHUMIDIFIER: ThinQHumidifierEntityDescription(
key=ThinQProperty.TARGET_HUMIDITY,
name=None,
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
translation_key="dehumidifier",
current_humidity_key=ThinQProperty.CURRENT_HUMIDITY,
operation_key=ThinQProperty.DEHUMIDIFIER_OPERATION_MODE,
),
DeviceType.HUMIDIFIER: ThinQHumidifierEntityDescription(
key=ThinQProperty.TARGET_HUMIDITY,
name=None,
device_class=HumidifierDeviceClass.HUMIDIFIER,
translation_key="humidifier",
current_humidity_key=ThinQProperty.HUMIDITY,
operation_key=ThinQProperty.HUMIDIFIER_OPERATION_MODE,
),
}
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ThinqConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up an entry for humidifier platform."""
entities: list[ThinQHumidifierEntity] = []
for coordinator in entry.runtime_data.coordinators.values():
if (
description := DEVICE_TYPE_HUM_MAP.get(coordinator.api.device.device_type)
) is not None:
entities.extend(
ThinQHumidifierEntity(coordinator, description, property_id)
for property_id in coordinator.api.get_active_idx(
description.key, ActiveMode.READ_WRITE
)
)
if entities:
async_add_entities(entities)
class ThinQHumidifierEntity(ThinQEntity, HumidifierEntity):
"""Represent a ThinQ humidifier entity."""
entity_description: ThinQHumidifierEntityDescription
_attr_supported_features = HumidifierEntityFeature.MODES
def __init__(
self,
coordinator: DeviceDataUpdateCoordinator,
entity_description: ThinQHumidifierEntityDescription,
property_id: str,
) -> None:
"""Initialize a humidifier entity."""
super().__init__(coordinator, entity_description, property_id)
self._attr_available_modes = self.coordinator.data[
self.entity_description.mode_key
].options
if self.data.max is not None:
self._attr_max_humidity = self.data.max
if self.data.min is not None:
self._attr_min_humidity = self.data.min
self._attr_target_humidity_step = (
self.data.step if self.data.step is not None else 1
)
def _update_status(self) -> None:
"""Update status itself."""
super()._update_status()
self._attr_target_humidity = self.data.value
self._attr_current_humidity = self.coordinator.data[
self.entity_description.current_humidity_key
].value
self._attr_is_on = self.coordinator.data[
self.entity_description.operation_key
].is_on
self._attr_mode = self.coordinator.data[self.entity_description.mode_key].value
if self.is_on:
self._attr_action = (
HumidifierAction.DRYING
if self.entity_description.device_class
== HumidifierDeviceClass.DEHUMIDIFIER
else HumidifierAction.HUMIDIFYING
)
else:
self._attr_action = HumidifierAction.OFF
_LOGGER.debug(
"[%s:%s] update status: c:%s, t:%s, mode:%s, action:%s, is_on:%s",
self.coordinator.device_name,
self.property_id,
self.current_humidity,
self.target_humidity,
self.mode,
self.action,
self.is_on,
)
async def async_set_mode(self, mode: str) -> None:
"""Set new target preset mode."""
_LOGGER.debug(
"[%s:%s] async_set_mode: %s",
self.coordinator.device_name,
self.entity_description.mode_key,
mode,
)
await self.async_call_api(
self.coordinator.api.post(self.entity_description.mode_key, mode)
)
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
_target_humidity = round(humidity / (self.target_humidity_step or 1)) * (
self.target_humidity_step or 1
)
_LOGGER.debug(
"[%s:%s] async_set_humidity: %s, target_humidity: %s, step: %s",
self.coordinator.device_name,
self.property_id,
humidity,
_target_humidity,
self.target_humidity_step,
)
if _target_humidity == self.target_humidity:
return
await self.async_call_api(
self.coordinator.api.post(self.property_id, _target_humidity)
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
if self.is_on:
return
_LOGGER.debug(
"[%s:%s] async_turn_on",
self.coordinator.device_name,
self.entity_description.operation_key,
)
await self.async_call_api(
self.coordinator.api.async_turn_on(self.entity_description.operation_key)
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
if not self.is_on:
return
_LOGGER.debug(
"[%s:%s] async_turn_off",
self.coordinator.device_name,
self.entity_description.operation_key,
)
await self.async_call_api(
self.coordinator.api.async_turn_off(self.entity_description.operation_key)
)

View File

@@ -199,33 +199,6 @@
}
}
},
"humidifier": {
"dehumidifier": {
"state_attributes": {
"mode": {
"state": {
"air_clean": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::air_clean%]",
"clothes_dry": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::clothes_dry%]",
"intensive_dry": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::intensive_dry%]",
"quiet_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::quiet_humidity%]",
"rapid_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::rapid_humidity%]",
"smart_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::smart_humidity%]"
}
}
}
},
"humidifier": {
"state_attributes": {
"mode": {
"state": {
"air_clean": "[%key:component::lg_thinq::entity::select::current_job_mode::state::air_clean%]",
"humidify": "[%key:component::lg_thinq::entity::select::current_job_mode::state::humidify%]",
"humidify_and_air_clean": "[%key:component::lg_thinq::entity::select::current_job_mode::state::humidify_and_air_clean%]"
}
}
}
}
},
"number": {
"fan_speed": {
"name": "Fan"

View File

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

View File

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

View File

@@ -1,9 +1,5 @@
"""Support for Netatmo binary sensors."""
from dataclasses import dataclass
import logging
from typing import Final, cast
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
@@ -13,33 +9,17 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import NETATMO_CREATE_WEATHER_BINARY_SENSOR
from .const import NETATMO_CREATE_WEATHER_SENSOR
from .data_handler import NetatmoDevice
from .entity import NetatmoWeatherModuleEntity
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class NetatmoBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Netatmo binary sensor entity."""
name: str | None = None # The default name of the sensor
netatmo_name: str # The name used by Netatmo API for this sensor
NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS: Final[
list[NetatmoBinarySensorEntityDescription]
] = [
NetatmoBinarySensorEntityDescription(
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="reachable",
name="Connectivity",
netatmo_name="reachable",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
]
)
async def async_setup_entry(
@@ -47,75 +27,36 @@ async def async_setup_entry(
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Netatmo weather binary sensors based on a config entry."""
"""Set up Netatmo binary sensors based on a config entry."""
@callback
def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None:
"""Create weather binary sensor entities for a Netatmo weather device."""
descriptions_to_add = NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS
entities: list[NetatmoWeatherBinarySensor] = []
# Create binary sensors for module
for description in descriptions_to_add:
# Actual check is simple for reachable
feature_check = description.key
if feature_check in netatmo_device.device.features:
_LOGGER.debug(
'Adding "%s" weather binary sensor for device %s',
feature_check,
netatmo_device.device.name,
)
entities.append(
NetatmoWeatherBinarySensor(
netatmo_device,
description,
)
)
if entities:
async_add_entities(entities)
async_add_entities(
NetatmoWeatherBinarySensor(netatmo_device, description)
for description in BINARY_SENSOR_TYPES
if description.key in netatmo_device.device.features
)
entry.async_on_unload(
async_dispatcher_connect(
hass,
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
_create_weather_binary_sensor_entity,
hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_binary_sensor_entity
)
)
class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity):
"""Implementation of a Netatmo weather binary sensor."""
entity_description: NetatmoBinarySensorEntityDescription
"""Implementation of a Netatmo binary sensor."""
def __init__(
self,
netatmo_device: NetatmoDevice,
description: NetatmoBinarySensorEntityDescription,
self, device: NetatmoDevice, description: BinarySensorEntityDescription
) -> None:
"""Initialize a Netatmo weather binary sensor."""
super().__init__(netatmo_device)
"""Initialize a Netatmo binary sensor."""
super().__init__(device)
self.entity_description = description
self._attr_unique_id = f"{self.device.entity_id}-{description.key}"
@callback
def async_update_callback(self) -> None:
"""Update the entity's state."""
value: StateType | None = None
value = getattr(self.device, self.entity_description.netatmo_name, None)
if value is None:
self._attr_available = False
self._attr_is_on = False
else:
self._attr_available = True
self._attr_is_on = cast(bool, value)
self._attr_is_on = self.device.reachable
self.async_write_ha_state()

View File

@@ -53,7 +53,6 @@ NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor"
NETATMO_CREATE_SELECT = "netatmo_create_select"
NETATMO_CREATE_SENSOR = "netatmo_create_sensor"
NETATMO_CREATE_SWITCH = "netatmo_create_switch"
NETATMO_CREATE_WEATHER_BINARY_SENSOR = "netatmo_create_weather_binary_sensor"
NETATMO_CREATE_WEATHER_SENSOR = "netatmo_create_weather_sensor"
CONF_AREA_NAME = "area_name"

View File

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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -1,103 +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: |
Use load_json_object_fixture in tests
Patch the library instead of the HTTP requests
Create a shared fixture for the mock config entry
Use init_integration in tests
Evaluate the need of test_config_entry_not_ready
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
This integration does use a cloud service.
discovery:
status: exempt
comment: |
This integration does use a cloud service.
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices:
status: exempt
comment: |
This integration does not use devices.
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: done
entity-category: todo
entity-device-class:
status: todo
comment: |
Extract attributes into own entities.
entity-disabled-by-default: done
entity-translations: todo
exception-translations: todo
icon-translations:
status: exempt
comment: |
This integration does not custom icons.
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration does not use issues.
stale-devices:
status: exempt
comment: |
This integration does not use devices.
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo

View File

@@ -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"]
}

View File

@@ -28,15 +28,9 @@
"exchange_rate": {
"default": "mdi:currency-usd"
},
"highest_price": {
"default": "mdi:cash-plus"
},
"last_price": {
"default": "mdi:cash"
},
"lowest_price": {
"default": "mdi:cash-minus"
},
"next_price": {
"default": "mdi:cash"
},

View File

@@ -2,23 +2,23 @@
from __future__ import annotations
from openevsehttp.__main__ import OpenEVSE
import openevsewifi
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
type OpenEVSEConfigEntry = ConfigEntry[OpenEVSE]
type OpenEVSEConfigEntry = ConfigEntry[openevsewifi.Charger]
async def async_setup_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) -> bool:
"""Set up openevse from a config entry."""
entry.runtime_data = OpenEVSE(entry.data[CONF_HOST])
entry.runtime_data = openevsewifi.Charger(entry.data[CONF_HOST])
try:
await entry.runtime_data.test_and_get()
except TimeoutError as ex:
await hass.async_add_executor_job(entry.runtime_data.getStatus)
except AttributeError as ex:
raise ConfigEntryError("Unable to connect to charger") from ex
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])

View File

@@ -2,14 +2,13 @@
from typing import Any
from openevsehttp.__main__ import OpenEVSE
import openevsewifi
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.helpers.service_info import zeroconf
from homeassistant.const import CONF_HOST
from .const import CONF_ID, CONF_SERIAL, DOMAIN
from .const import DOMAIN
class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -18,33 +17,27 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 1
def __init__(self) -> None:
"""Set up the instance."""
self.discovery_info: dict[str, Any] = {}
async def check_status(self, host: str) -> tuple[bool, str | None]:
async def check_status(self, host: str) -> bool:
"""Check if we can connect to the OpenEVSE charger."""
charger = OpenEVSE(host)
charger = openevsewifi.Charger(host)
try:
result = await charger.test_and_get()
except TimeoutError:
return False, None
return True, result.get(CONF_SERIAL)
result = await self.hass.async_add_executor_job(charger.getStatus)
except AttributeError:
return False
else:
return result is not None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
errors = None
if user_input is not None:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
if (result := await self.check_status(user_input[CONF_HOST]))[0]:
if (serial := result[1]) is not None:
await self.async_set_unique_id(serial, raise_on_progress=False)
self._abort_if_unique_id_configured()
if await self.check_status(user_input[CONF_HOST]):
return self.async_create_entry(
title=f"OpenEVSE {user_input[CONF_HOST]}",
data=user_input,
@@ -62,53 +55,10 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: data[CONF_HOST]})
if (result := await self.check_status(data[CONF_HOST]))[0]:
if (serial := result[1]) is not None:
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()
else:
if not await self.check_status(data[CONF_HOST]):
return self.async_abort(reason="unavailable_host")
return self.async_create_entry(
title=f"OpenEVSE {data[CONF_HOST]}",
data=data,
)
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
self._async_abort_entries_match({CONF_HOST: discovery_info.host})
await self.async_set_unique_id(discovery_info.properties[CONF_ID])
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
host = discovery_info.host
name = f"OpenEVSE {discovery_info.name.split('.')[0]}"
self.discovery_info.update(
{
CONF_HOST: host,
CONF_NAME: name,
}
)
self.context.update({"title_placeholders": {"name": name}})
if not (await self.check_status(host))[0]:
return self.async_abort(reason="cannot_connect")
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is None:
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={"name": self.discovery_info[CONF_NAME]},
)
return self.async_create_entry(
title=self.discovery_info[CONF_NAME],
data={CONF_HOST: self.discovery_info[CONF_HOST]},
)

View File

@@ -1,6 +1,4 @@
"""Constants for the OpenEVSE integration."""
CONF_ID = "id"
CONF_SERIAL = "serial"
DOMAIN = "openevse"
INTEGRATION_TITLE = "OpenEVSE"

View File

@@ -1,14 +1,12 @@
{
"domain": "openevse",
"name": "OpenEVSE",
"after_dependencies": ["zeroconf"],
"codeowners": ["@c00w", "@firstof9"],
"codeowners": ["@c00w"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/openevse",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["openevsehttp"],
"loggers": ["openevsewifi"],
"quality_scale": "legacy",
"requirements": ["python-openevse-http==0.2.1"],
"zeroconf": ["_openevse._tcp.local."]
"requirements": ["openevsewifi==1.1.2"]
}

View File

@@ -4,7 +4,8 @@ from __future__ import annotations
import logging
from openevsehttp.__main__ import OpenEVSE
import openevsewifi
from requests import RequestException
import voluptuous as vol
from homeassistant.components.sensor import (
@@ -17,8 +18,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 +27,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 +41,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 +67,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 +75,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 +159,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,54 +172,36 @@ async def async_setup_entry(
class OpenEVSESensor(SensorEntity):
"""Implementation of an OpenEVSE sensor."""
_attr_has_entity_name = True
def __init__(
self,
charger: OpenEVSE,
host: str,
charger: openevsewifi.Charger,
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:
def update(self) -> None:
"""Get the monitored data from the charger."""
try:
await self.charger.update()
except TimeoutError:
sensor_type = self.entity_description.key
if sensor_type == "status":
self._attr_native_value = self.charger.getStatus()
elif sensor_type == "charge_time":
self._attr_native_value = self.charger.getChargeTimeElapsed() / 60
elif sensor_type == "ambient_temp":
self._attr_native_value = self.charger.getAmbientTemperature()
elif sensor_type == "ir_temp":
self._attr_native_value = self.charger.getIRTemperature()
elif sensor_type == "rtc_temp":
self._attr_native_value = self.charger.getRTCTemperature()
elif sensor_type == "usage_session":
self._attr_native_value = float(self.charger.getUsageSession()) / 1000
elif sensor_type == "usage_total":
self._attr_native_value = float(self.charger.getUsageTotal()) / 1000
else:
self._attr_native_value = "Unknown"
except (RequestException, ValueError, KeyError):
_LOGGER.warning("Could not update status for %s", self.name)
return
sensor_type = self.entity_description.key
if sensor_type == "status":
self._attr_native_value = self.charger.status
elif sensor_type == "charge_time":
self._attr_native_value = self.charger.charge_time_elapsed / 60
elif sensor_type == "ambient_temp":
self._attr_native_value = self.charger.ambient_temperature
elif sensor_type == "ir_temp":
self._attr_native_value = self.charger.ir_temperature
elif sensor_type == "rtc_temp":
self._attr_native_value = self.charger.rtc_temperature
elif sensor_type == "usage_session":
self._attr_native_value = float(self.charger.usage_session) / 1000
elif sensor_type == "usage_total":
self._attr_native_value = float(self.charger.usage_total) / 1000
else:
self._attr_native_value = "Unknown"

View File

@@ -13,36 +13,11 @@
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "Enter the IP address of your OpenEVSE. Should match the address you used to set it up."
"host": "Enter the IP Address of your openevse. Should match the address you used to set it up."
}
}
}
},
"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.",

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.16.1"]
"requirements": ["opower==0.16.0"]
}

View File

@@ -55,7 +55,7 @@ SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = {
key="optimization_mode",
translation_key="optimization_mode",
device_class=SensorDeviceClass.ENUM,
options=["off", "oso", "gridcompany", "smartcompany", "advanced", "nettleie"],
options=["off", "oso", "gridcompany", "smartcompany", "advanced"],
value_fn=lambda entity_data: entity_data.state.lower(),
),
"power_load": OSOEnergySensorEntityDescription(

View File

@@ -58,7 +58,6 @@
"state": {
"advanced": "Advanced",
"gridcompany": "Grid company",
"nettleie": "Grid fee",
"off": "[%key:common::state::off%]",
"oso": "OSO",
"smartcompany": "Smart company"

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.7.1"]
"requirements": ["python-otbr-api==2.7.0"]
}

View File

@@ -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:

View File

@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.19.4"],
"requirements": ["pyoverkiz==1.19.3"],
"zeroconf": [
{
"name": "gateway*",

View File

@@ -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

View File

@@ -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."
}
}
}

View File

@@ -11,11 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import (
PowerfoxConfigEntry,
PowerfoxDataUpdateCoordinator,
PowerfoxReportDataUpdateCoordinator,
)
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
@@ -34,16 +30,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) ->
await client.close()
raise ConfigEntryNotReady from err
coordinators: list[
PowerfoxDataUpdateCoordinator | PowerfoxReportDataUpdateCoordinator
] = []
for device in devices:
if device.type == DeviceType.GAS_METER:
coordinators.append(
PowerfoxReportDataUpdateCoordinator(hass, entry, client, device)
)
continue
coordinators.append(PowerfoxDataUpdateCoordinator(hass, entry, client, device))
coordinators: list[PowerfoxDataUpdateCoordinator] = [
PowerfoxDataUpdateCoordinator(hass, entry, client, device)
for device in devices
# Filter out gas meter devices (Powerfox FLOW adapters) as they are not yet supported and cause integration failures
if device.type != DeviceType.GAS_METER
]
await asyncio.gather(
*[

View File

@@ -2,11 +2,8 @@
from __future__ import annotations
from datetime import datetime
from powerfox import (
Device,
DeviceReport,
Powerfox,
PowerfoxAuthenticationError,
PowerfoxConnectionError,
@@ -18,18 +15,14 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
type PowerfoxCoordinator = (
"PowerfoxDataUpdateCoordinator" | "PowerfoxReportDataUpdateCoordinator"
)
type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxCoordinator]]
type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxDataUpdateCoordinator]]
class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
"""Base coordinator handling shared Powerfox logic."""
class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
"""Class to manage fetching Powerfox data from the API."""
config_entry: PowerfoxConfigEntry
@@ -40,7 +33,7 @@ class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
client: Powerfox,
device: Device,
) -> None:
"""Initialize shared Powerfox coordinator."""
"""Initialize global Powerfox data updater."""
super().__init__(
hass,
LOGGER,
@@ -51,37 +44,11 @@ class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
self.client = client
self.device = device
async def _async_update_data(self) -> T:
"""Fetch data and normalize Powerfox errors."""
async def _async_update_data(self) -> Poweropti:
"""Fetch data from Powerfox API."""
try:
return await self._async_fetch_data()
return await self.client.device(device_id=self.device.id)
except PowerfoxAuthenticationError as err:
raise ConfigEntryAuthFailed(err) from err
except (PowerfoxConnectionError, PowerfoxNoDataError) as err:
raise UpdateFailed(err) from err
async def _async_fetch_data(self) -> T:
"""Fetch data from the Powerfox API."""
raise NotImplementedError
class PowerfoxDataUpdateCoordinator(PowerfoxBaseCoordinator[Poweropti]):
"""Class to manage fetching Powerfox data from the API."""
async def _async_fetch_data(self) -> Poweropti:
"""Fetch live device data from the Powerfox API."""
return await self.client.device(device_id=self.device.id)
class PowerfoxReportDataUpdateCoordinator(PowerfoxBaseCoordinator[DeviceReport]):
"""Coordinator handling report data from the API."""
async def _async_fetch_data(self) -> DeviceReport:
"""Fetch report data from the Powerfox API."""
local_now = datetime.now(tz=dt_util.get_time_zone(self.hass.config.time_zone))
return await self.client.report(
device_id=self.device.id,
year=local_now.year,
month=local_now.month,
day=local_now.day,
)

View File

@@ -5,18 +5,18 @@ from __future__ import annotations
from datetime import datetime
from typing import Any
from powerfox import DeviceReport, HeatMeter, PowerMeter, WaterMeter
from powerfox import HeatMeter, PowerMeter, WaterMeter
from homeassistant.core import HomeAssistant
from .coordinator import PowerfoxConfigEntry
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: PowerfoxConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for Powerfox config entry."""
powerfox_data = entry.runtime_data
powerfox_data: list[PowerfoxDataUpdateCoordinator] = entry.runtime_data
return {
"devices": [
@@ -68,21 +68,6 @@ async def async_get_config_entry_diagnostics(
if isinstance(coordinator.data, HeatMeter)
else {}
),
**(
{
"gas_meter": {
"sum": coordinator.data.gas.sum,
"consumption": coordinator.data.gas.consumption,
"consumption_kwh": coordinator.data.gas.consumption_kwh,
"current_consumption": coordinator.data.gas.current_consumption,
"current_consumption_kwh": coordinator.data.gas.current_consumption_kwh,
"sum_currency": coordinator.data.gas.sum_currency,
}
}
if isinstance(coordinator.data, DeviceReport)
and coordinator.data.gas
else {}
),
}
for coordinator in powerfox_data
],

View File

@@ -2,27 +2,23 @@
from __future__ import annotations
from typing import Any
from powerfox import Device
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import PowerfoxBaseCoordinator
from .coordinator import PowerfoxDataUpdateCoordinator
class PowerfoxEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]](
CoordinatorEntity[CoordinatorT]
):
class PowerfoxEntity(CoordinatorEntity[PowerfoxDataUpdateCoordinator]):
"""Base entity for Powerfox."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: CoordinatorT,
coordinator: PowerfoxDataUpdateCoordinator,
device: Device,
) -> None:
"""Initialize Powerfox entity."""

View File

@@ -70,7 +70,10 @@ rules:
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-disabled-by-default:
status: exempt
comment: |
This integration does not have any entities that should disabled by default.
entity-translations: done
exception-translations: done
icon-translations:

View File

@@ -4,9 +4,8 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from powerfox import Device, GasReport, HeatMeter, PowerMeter, WaterMeter
from powerfox import Device, HeatMeter, PowerMeter, WaterMeter
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -14,16 +13,11 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import CURRENCY_EURO, UnitOfEnergy, UnitOfPower, UnitOfVolume
from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import (
PowerfoxBaseCoordinator,
PowerfoxConfigEntry,
PowerfoxDataUpdateCoordinator,
PowerfoxReportDataUpdateCoordinator,
)
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
from .entity import PowerfoxEntity
@@ -36,13 +30,6 @@ class PowerfoxSensorEntityDescription[T: (PowerMeter, WaterMeter, HeatMeter)](
value_fn: Callable[[T], float | int | None]
@dataclass(frozen=True, kw_only=True)
class PowerfoxReportSensorEntityDescription(SensorEntityDescription):
"""Describes Powerfox report sensor entity."""
value_fn: Callable[[GasReport], float | int | None]
SENSORS_POWER: tuple[PowerfoxSensorEntityDescription[PowerMeter], ...] = (
PowerfoxSensorEntityDescription[PowerMeter](
key="power",
@@ -139,104 +126,6 @@ SENSORS_HEAT: tuple[PowerfoxSensorEntityDescription[HeatMeter], ...] = (
),
)
SENSORS_GAS: tuple[PowerfoxReportSensorEntityDescription, ...] = (
PowerfoxReportSensorEntityDescription(
key="gas_consumption_today",
translation_key="gas_consumption_today",
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda gas: gas.sum,
),
PowerfoxReportSensorEntityDescription(
key="gas_consumption_energy_today",
translation_key="gas_consumption_energy_today",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
value_fn=lambda gas: gas.consumption_kwh,
),
PowerfoxReportSensorEntityDescription(
key="gas_current_consumption",
translation_key="gas_current_consumption",
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
value_fn=lambda gas: gas.current_consumption,
),
PowerfoxReportSensorEntityDescription(
key="gas_current_consumption_energy",
translation_key="gas_current_consumption_energy",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=lambda gas: gas.current_consumption_kwh,
),
PowerfoxReportSensorEntityDescription(
key="gas_cost_today",
translation_key="gas_cost_today",
native_unit_of_measurement=CURRENCY_EURO,
device_class=SensorDeviceClass.MONETARY,
suggested_display_precision=2,
state_class=SensorStateClass.TOTAL,
value_fn=lambda gas: gas.sum_currency,
),
PowerfoxReportSensorEntityDescription(
key="gas_max_consumption_today",
translation_key="gas_max_consumption_today",
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
value_fn=lambda gas: gas.max_consumption,
),
PowerfoxReportSensorEntityDescription(
key="gas_min_consumption_today",
translation_key="gas_min_consumption_today",
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
value_fn=lambda gas: gas.min_consumption,
),
PowerfoxReportSensorEntityDescription(
key="gas_avg_consumption_today",
translation_key="gas_avg_consumption_today",
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
entity_registry_enabled_default=False,
value_fn=lambda gas: gas.avg_consumption,
),
PowerfoxReportSensorEntityDescription(
key="gas_max_consumption_energy_today",
translation_key="gas_max_consumption_energy_today",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=lambda gas: gas.max_consumption_kwh,
),
PowerfoxReportSensorEntityDescription(
key="gas_min_consumption_energy_today",
translation_key="gas_min_consumption_energy_today",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=lambda gas: gas.min_consumption_kwh,
),
PowerfoxReportSensorEntityDescription(
key="gas_avg_consumption_energy_today",
translation_key="gas_avg_consumption_energy_today",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=lambda gas: gas.avg_consumption_kwh,
),
PowerfoxReportSensorEntityDescription(
key="gas_max_cost_today",
translation_key="gas_max_cost_today",
native_unit_of_measurement=CURRENCY_EURO,
device_class=SensorDeviceClass.MONETARY,
suggested_display_precision=2,
value_fn=lambda gas: gas.max_currency,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -246,20 +135,6 @@ async def async_setup_entry(
"""Set up Powerfox sensors based on a config entry."""
entities: list[SensorEntity] = []
for coordinator in entry.runtime_data:
if isinstance(coordinator, PowerfoxReportDataUpdateCoordinator):
gas_report = coordinator.data.gas
if gas_report is None:
continue
entities.extend(
PowerfoxGasSensorEntity(
coordinator=coordinator,
description=description,
device=coordinator.device,
)
for description in SENSORS_GAS
if description.value_fn(gas_report) is not None
)
continue
if isinstance(coordinator.data, PowerMeter):
entities.extend(
PowerfoxSensorEntity(
@@ -291,49 +166,23 @@ async def async_setup_entry(
async_add_entities(entities)
class BasePowerfoxSensorEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]](
PowerfoxEntity[CoordinatorT], SensorEntity
):
"""Common base for Powerfox sensor entities."""
class PowerfoxSensorEntity(PowerfoxEntity, SensorEntity):
"""Defines a powerfox power meter sensor."""
entity_description: SensorEntityDescription
entity_description: PowerfoxSensorEntityDescription
def __init__(
self,
coordinator: CoordinatorT,
coordinator: PowerfoxDataUpdateCoordinator,
device: Device,
description: SensorEntityDescription,
description: PowerfoxSensorEntityDescription,
) -> None:
"""Initialize the shared Powerfox sensor."""
"""Initialize Powerfox power meter sensor."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{device.id}_{description.key}"
class PowerfoxSensorEntity(BasePowerfoxSensorEntity[PowerfoxDataUpdateCoordinator]):
"""Defines a powerfox poweropti sensor."""
coordinator: PowerfoxDataUpdateCoordinator
entity_description: PowerfoxSensorEntityDescription
@property
def native_value(self) -> float | int | None:
"""Return the state of the entity."""
return self.entity_description.value_fn(self.coordinator.data)
class PowerfoxGasSensorEntity(
BasePowerfoxSensorEntity[PowerfoxReportDataUpdateCoordinator]
):
"""Defines a powerfox gas meter sensor."""
coordinator: PowerfoxReportDataUpdateCoordinator
entity_description: PowerfoxReportSensorEntityDescription
@property
def native_value(self) -> float | int | None:
"""Return the state of the entity."""
gas_report = self.coordinator.data.gas
if TYPE_CHECKING:
assert gas_report is not None
return self.entity_description.value_fn(gas_report)

View File

@@ -62,42 +62,6 @@
"energy_usage_low_tariff": {
"name": "Energy usage low tariff"
},
"gas_avg_consumption_energy_today": {
"name": "Avg gas hourly energy - today"
},
"gas_avg_consumption_today": {
"name": "Avg gas hourly consumption - today"
},
"gas_consumption_energy_today": {
"name": "Gas consumption energy - today"
},
"gas_consumption_today": {
"name": "Gas consumption - today"
},
"gas_cost_today": {
"name": "Gas cost - today"
},
"gas_current_consumption": {
"name": "Gas consumption - this hour"
},
"gas_current_consumption_energy": {
"name": "Gas consumption energy - this hour"
},
"gas_max_consumption_energy_today": {
"name": "Max gas hourly energy - today"
},
"gas_max_consumption_today": {
"name": "Max gas hourly consumption - today"
},
"gas_max_cost_today": {
"name": "Max gas hourly cost - today"
},
"gas_min_consumption_energy_today": {
"name": "Min gas hourly energy - today"
},
"gas_min_consumption_today": {
"name": "Min gas hourly consumption - today"
},
"heat_delta_energy": {
"name": "Delta energy"
},

View File

@@ -453,6 +453,10 @@ async def _async_generate_memory_profile(hass: HomeAssistant, call: ServiceCall)
# Imports deferred to avoid loading modules
# in memory since usually only one part of this
# integration is used at a time
if sys.version_info >= (3, 14):
raise HomeAssistantError(
"Memory profiling is not supported on Python 3.14. Please use Python 3.13."
)
from guppy import hpy # noqa: PLC0415
start_time = int(time.time() * 1000000)

View File

@@ -7,7 +7,7 @@
"quality_scale": "internal",
"requirements": [
"pyprof2calltree==1.4.5",
"guppy3==3.1.6",
"guppy3==3.1.5;python_version<'3.14'",
"objgraph==3.5.0"
],
"single_config_entry": true

View File

@@ -128,9 +128,8 @@ class RingCam(RingEntity[RingDoorBell], Camera):
self._device = self._get_coordinator_data().get_video_device(
self._device.device_api_id
)
history_data = self._device.last_history
if history_data and self._device.has_subscription:
if history_data:
self._last_event = history_data[0]
# will call async_update to update the attributes and get the
# video url from the api
@@ -155,16 +154,8 @@ class RingCam(RingEntity[RingDoorBell], Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
if self._video_url is None:
if not self._device.has_subscription:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="no_subscription",
)
return None
key = (width, height)
if not (image := self._images.get(key)):
if not (image := self._images.get(key)) and self._video_url is not None:
image = await ffmpeg.async_get_image(
self.hass,
self._video_url,

View File

@@ -151,9 +151,6 @@
"api_timeout": {
"message": "Timeout communicating with Ring API"
},
"no_subscription": {
"message": "Ring Protect subscription required for snapshots"
},
"sdp_m_line_index_required": {
"message": "Error negotiating stream for {device}"
}

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==4.2.1",
"python-roborock==4.2.0",
"vacuum-map-parser-roborock==0.1.4"
]
}

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