Add `charging enabled` switch to TechnoVE (#121484)
* Add session_active switch to TechnoVE * Replace multi-line lambda with function def * Make lambda one linepull/124512/head
parent
26e87509be
commit
79ba315008
|
@ -4,19 +4,28 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from technove import Station as TechnoVEStation
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from . import TechnoVEConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TechnoVEDataUpdateCoordinator
|
||||
from .entity import TechnoVEEntity
|
||||
|
||||
|
@ -25,6 +34,7 @@ from .entity import TechnoVEEntity
|
|||
class TechnoVEBinarySensorDescription(BinarySensorEntityDescription):
|
||||
"""Describes TechnoVE binary sensor entity."""
|
||||
|
||||
deprecated_version: str | None = None
|
||||
value_fn: Callable[[TechnoVEStation], bool | None]
|
||||
|
||||
|
||||
|
@ -52,6 +62,9 @@ BINARY_SENSORS = [
|
|||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
value_fn=lambda station: station.info.is_session_active,
|
||||
deprecated_version="2025.2.0",
|
||||
# Disabled by default, as this entity is deprecated
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TechnoVEBinarySensorDescription(
|
||||
key="is_static_ip",
|
||||
|
@ -100,3 +113,34 @@ class TechnoVEBinarySensorEntity(TechnoVEEntity, BinarySensorEntity):
|
|||
"""Return the state of the sensor."""
|
||||
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Raise issue when entity is registered and was not disabled."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.unique_id
|
||||
if entity_id := er.async_get(self.hass).async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, self.unique_id
|
||||
):
|
||||
if self.enabled and self.entity_description.deprecated_version:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_entity_{self.entity_description.key}",
|
||||
breaks_in_ha_version=self.entity_description.deprecated_version,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_entity_{self.entity_description.key}",
|
||||
translation_placeholders={
|
||||
"sensor_name": self.name
|
||||
if isinstance(self.name, str)
|
||||
else entity_id,
|
||||
"entity": entity_id,
|
||||
},
|
||||
)
|
||||
else:
|
||||
async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_entity_{self.entity_description.key}",
|
||||
)
|
||||
await super().async_added_to_hass()
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
"ssid": {
|
||||
"default": "mdi:wifi"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"session_active": {
|
||||
"default": "mdi:ev-station"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,12 +77,24 @@
|
|||
"switch": {
|
||||
"auto_charge": {
|
||||
"name": "Auto charge"
|
||||
},
|
||||
"session_active": {
|
||||
"name": "Charging Enabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"max_current_in_sharing_mode": {
|
||||
"message": "Cannot set the max current when power sharing mode is enabled."
|
||||
},
|
||||
"set_charging_enabled_on_auto_charge": {
|
||||
"message": "Cannot enable or disable charging when auto-charge is enabled. Try disabling auto-charge first."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_entity_is_session_active": {
|
||||
"title": "The TechnoVE `{sensor_name}` binary sensor is deprecated",
|
||||
"description": "`{entity}` is deprecated.\nPlease update your automations and scripts to replace the binary sensor entity with the newly added switch entity.\nWhen you are done migrating you can disable `{entity}`."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,30 +2,59 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from technove import Station as TechnoVEStation, TechnoVE
|
||||
from technove import Station as TechnoVEStation
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import TechnoVEConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TechnoVEDataUpdateCoordinator
|
||||
from .entity import TechnoVEEntity
|
||||
from .helpers import technove_exception_handler
|
||||
|
||||
|
||||
async def _set_charging_enabled(
|
||||
coordinator: TechnoVEDataUpdateCoordinator, enabled: bool
|
||||
) -> None:
|
||||
if coordinator.data.info.auto_charge:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_charging_enabled_on_auto_charge",
|
||||
)
|
||||
await coordinator.technove.set_charging_enabled(enabled=enabled)
|
||||
coordinator.data.info.is_session_active = enabled
|
||||
coordinator.async_set_updated_data(coordinator.data)
|
||||
|
||||
|
||||
async def _enable_charging(coordinator: TechnoVEDataUpdateCoordinator) -> None:
|
||||
await _set_charging_enabled(coordinator, True)
|
||||
|
||||
|
||||
async def _disable_charging(coordinator: TechnoVEDataUpdateCoordinator) -> None:
|
||||
await _set_charging_enabled(coordinator, False)
|
||||
|
||||
|
||||
async def _set_auto_charge(
|
||||
coordinator: TechnoVEDataUpdateCoordinator, enabled: bool
|
||||
) -> None:
|
||||
await coordinator.technove.set_auto_charge(enabled=enabled)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TechnoVESwitchDescription(SwitchEntityDescription):
|
||||
"""Describes TechnoVE binary sensor entity."""
|
||||
|
||||
is_on_fn: Callable[[TechnoVEStation], bool]
|
||||
turn_on_fn: Callable[[TechnoVE], Awaitable[dict[str, Any]]]
|
||||
turn_off_fn: Callable[[TechnoVE], Awaitable[dict[str, Any]]]
|
||||
turn_on_fn: Callable[[TechnoVEDataUpdateCoordinator], Coroutine[Any, Any, None]]
|
||||
turn_off_fn: Callable[[TechnoVEDataUpdateCoordinator], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
SWITCHES = [
|
||||
|
@ -34,8 +63,16 @@ SWITCHES = [
|
|||
translation_key="auto_charge",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda station: station.info.auto_charge,
|
||||
turn_on_fn=lambda technoVE: technoVE.set_auto_charge(enabled=True),
|
||||
turn_off_fn=lambda technoVE: technoVE.set_auto_charge(enabled=False),
|
||||
turn_on_fn=lambda coordinator: _set_auto_charge(coordinator, True),
|
||||
turn_off_fn=lambda coordinator: _set_auto_charge(coordinator, False),
|
||||
),
|
||||
TechnoVESwitchDescription(
|
||||
key="session_active",
|
||||
translation_key="session_active",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda station: station.info.is_session_active,
|
||||
turn_on_fn=_enable_charging,
|
||||
turn_off_fn=_disable_charging,
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -76,11 +113,9 @@ class TechnoVESwitchEntity(TechnoVEEntity, SwitchEntity):
|
|||
@technove_exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the TechnoVE switch."""
|
||||
await self.entity_description.turn_on_fn(self.coordinator.technove)
|
||||
await self.coordinator.async_request_refresh()
|
||||
await self.entity_description.turn_on_fn(self.coordinator)
|
||||
|
||||
@technove_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the TechnoVE switch."""
|
||||
await self.entity_description.turn_off_fn(self.coordinator.technove)
|
||||
await self.coordinator.async_request_refresh()
|
||||
await self.entity_description.turn_off_fn(self.coordinator)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"current": 23.75,
|
||||
"network_ssid": "Connecting...",
|
||||
"id": "AA:AA:AA:AA:AA:BB",
|
||||
"auto_charge": true,
|
||||
"auto_charge": false,
|
||||
"highChargePeriodActive": false,
|
||||
"normalPeriodActive": false,
|
||||
"maxChargePourcentage": 0.9,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'auto_charge': True,
|
||||
'auto_charge': False,
|
||||
'conflict_in_sharing_config': False,
|
||||
'current': 23.75,
|
||||
'energy_session': 12.34,
|
||||
|
|
|
@ -42,6 +42,52 @@
|
|||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[switch.technove_station_charging_enabled-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.technove_station_charging_enabled',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Charging Enabled',
|
||||
'platform': 'technove',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'session_active',
|
||||
'unique_id': 'AA:AA:AA:AA:AA:BB_session_active',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[switch.technove_station_charging_enabled-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'TechnoVE Station Charging Enabled',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.technove_station_charging_enabled',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
|||
from syrupy import SnapshotAssertion
|
||||
from technove import TechnoVEError
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
@ -43,7 +43,10 @@ async def test_sensors(
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id",
|
||||
["binary_sensor.technove_station_static_ip"],
|
||||
[
|
||||
"binary_sensor.technove_station_static_ip",
|
||||
"binary_sensor.technove_station_charging",
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_disabled_by_default_binary_sensors(
|
||||
|
@ -64,9 +67,9 @@ async def test_binary_sensor_update_failure(
|
|||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator update failure."""
|
||||
entity_id = "binary_sensor.technove_station_charging"
|
||||
entity_id = "binary_sensor.technove_station_power_sharing_mode"
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
mock_technove.update.side_effect = TechnoVEError("Test error")
|
||||
freezer.tick(timedelta(minutes=5, seconds=1))
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
@ -53,6 +53,12 @@ async def test_switches(
|
|||
{"enabled": True},
|
||||
{"enabled": False},
|
||||
),
|
||||
(
|
||||
"switch.technove_station_charging_enabled",
|
||||
"set_charging_enabled",
|
||||
{"enabled": True},
|
||||
{"enabled": False},
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
|
@ -96,6 +102,10 @@ async def test_switch_on_off(
|
|||
"switch.technove_station_auto_charge",
|
||||
"set_auto_charge",
|
||||
),
|
||||
(
|
||||
"switch.technove_station_charging_enabled",
|
||||
"set_charging_enabled",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
|
@ -130,6 +140,10 @@ async def test_invalid_response(
|
|||
"switch.technove_station_auto_charge",
|
||||
"set_auto_charge",
|
||||
),
|
||||
(
|
||||
"switch.technove_station_charging_enabled",
|
||||
"set_charging_enabled",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
|
@ -157,3 +171,31 @@ async def test_connection_error(
|
|||
assert method_mock.call_count == 1
|
||||
assert (state := hass.states.get(state.entity_id))
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_disable_charging_auto_charge(
|
||||
hass: HomeAssistant,
|
||||
mock_technove: MagicMock,
|
||||
) -> None:
|
||||
"""Test failure to disable charging when the station is in auto charge mode."""
|
||||
entity_id = "switch.technove_station_charging_enabled"
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
# Enable auto-charge mode
|
||||
device = mock_technove.update.return_value
|
||||
device.info.auto_charge = True
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="auto-charge is enabled",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert (state := hass.states.get(state.entity_id))
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
|
Loading…
Reference in New Issue