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 collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from technove import Station as TechnoVEStation
|
from technove import Station as TechnoVEStation
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import (
|
||||||
|
IssueSeverity,
|
||||||
|
async_create_issue,
|
||||||
|
async_delete_issue,
|
||||||
|
)
|
||||||
|
|
||||||
from . import TechnoVEConfigEntry
|
from . import TechnoVEConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import TechnoVEDataUpdateCoordinator
|
from .coordinator import TechnoVEDataUpdateCoordinator
|
||||||
from .entity import TechnoVEEntity
|
from .entity import TechnoVEEntity
|
||||||
|
|
||||||
|
@ -25,6 +34,7 @@ from .entity import TechnoVEEntity
|
||||||
class TechnoVEBinarySensorDescription(BinarySensorEntityDescription):
|
class TechnoVEBinarySensorDescription(BinarySensorEntityDescription):
|
||||||
"""Describes TechnoVE binary sensor entity."""
|
"""Describes TechnoVE binary sensor entity."""
|
||||||
|
|
||||||
|
deprecated_version: str | None = None
|
||||||
value_fn: Callable[[TechnoVEStation], bool | None]
|
value_fn: Callable[[TechnoVEStation], bool | None]
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,6 +62,9 @@ BINARY_SENSORS = [
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||||
value_fn=lambda station: station.info.is_session_active,
|
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(
|
TechnoVEBinarySensorDescription(
|
||||||
key="is_static_ip",
|
key="is_static_ip",
|
||||||
|
@ -100,3 +113,34 @@ class TechnoVEBinarySensorEntity(TechnoVEEntity, BinarySensorEntity):
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
|
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
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": {
|
"ssid": {
|
||||||
"default": "mdi:wifi"
|
"default": "mdi:wifi"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"session_active": {
|
||||||
|
"default": "mdi:ev-station"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,12 +77,24 @@
|
||||||
"switch": {
|
"switch": {
|
||||||
"auto_charge": {
|
"auto_charge": {
|
||||||
"name": "Auto charge"
|
"name": "Auto charge"
|
||||||
|
},
|
||||||
|
"session_active": {
|
||||||
|
"name": "Charging Enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"max_current_in_sharing_mode": {
|
"max_current_in_sharing_mode": {
|
||||||
"message": "Cannot set the max current when power sharing mode is enabled."
|
"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 __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
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.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import TechnoVEConfigEntry
|
from . import TechnoVEConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import TechnoVEDataUpdateCoordinator
|
from .coordinator import TechnoVEDataUpdateCoordinator
|
||||||
from .entity import TechnoVEEntity
|
from .entity import TechnoVEEntity
|
||||||
from .helpers import technove_exception_handler
|
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)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class TechnoVESwitchDescription(SwitchEntityDescription):
|
class TechnoVESwitchDescription(SwitchEntityDescription):
|
||||||
"""Describes TechnoVE binary sensor entity."""
|
"""Describes TechnoVE binary sensor entity."""
|
||||||
|
|
||||||
is_on_fn: Callable[[TechnoVEStation], bool]
|
is_on_fn: Callable[[TechnoVEStation], bool]
|
||||||
turn_on_fn: Callable[[TechnoVE], Awaitable[dict[str, Any]]]
|
turn_on_fn: Callable[[TechnoVEDataUpdateCoordinator], Coroutine[Any, Any, None]]
|
||||||
turn_off_fn: Callable[[TechnoVE], Awaitable[dict[str, Any]]]
|
turn_off_fn: Callable[[TechnoVEDataUpdateCoordinator], Coroutine[Any, Any, None]]
|
||||||
|
|
||||||
|
|
||||||
SWITCHES = [
|
SWITCHES = [
|
||||||
|
@ -34,8 +63,16 @@ SWITCHES = [
|
||||||
translation_key="auto_charge",
|
translation_key="auto_charge",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
is_on_fn=lambda station: station.info.auto_charge,
|
is_on_fn=lambda station: station.info.auto_charge,
|
||||||
turn_on_fn=lambda technoVE: technoVE.set_auto_charge(enabled=True),
|
turn_on_fn=lambda coordinator: _set_auto_charge(coordinator, True),
|
||||||
turn_off_fn=lambda technoVE: technoVE.set_auto_charge(enabled=False),
|
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
|
@technove_exception_handler
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn on the TechnoVE switch."""
|
"""Turn on the TechnoVE switch."""
|
||||||
await self.entity_description.turn_on_fn(self.coordinator.technove)
|
await self.entity_description.turn_on_fn(self.coordinator)
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
|
|
||||||
@technove_exception_handler
|
@technove_exception_handler
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn off the TechnoVE switch."""
|
"""Turn off the TechnoVE switch."""
|
||||||
await self.entity_description.turn_off_fn(self.coordinator.technove)
|
await self.entity_description.turn_off_fn(self.coordinator)
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"current": 23.75,
|
"current": 23.75,
|
||||||
"network_ssid": "Connecting...",
|
"network_ssid": "Connecting...",
|
||||||
"id": "AA:AA:AA:AA:AA:BB",
|
"id": "AA:AA:AA:AA:AA:BB",
|
||||||
"auto_charge": true,
|
"auto_charge": false,
|
||||||
"highChargePeriodActive": false,
|
"highChargePeriodActive": false,
|
||||||
"normalPeriodActive": false,
|
"normalPeriodActive": false,
|
||||||
"maxChargePourcentage": 0.9,
|
"maxChargePourcentage": 0.9,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: test_diagnostics
|
# name: test_diagnostics
|
||||||
dict({
|
dict({
|
||||||
'auto_charge': True,
|
'auto_charge': False,
|
||||||
'conflict_in_sharing_config': False,
|
'conflict_in_sharing_config': False,
|
||||||
'current': 23.75,
|
'current': 23.75,
|
||||||
'energy_session': 12.34,
|
'energy_session': 12.34,
|
||||||
|
|
|
@ -42,6 +42,52 @@
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <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',
|
'state': 'on',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
from technove import TechnoVEError
|
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.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
@ -43,7 +43,10 @@ async def test_sensors(
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"entity_id",
|
"entity_id",
|
||||||
["binary_sensor.technove_station_static_ip"],
|
[
|
||||||
|
"binary_sensor.technove_station_static_ip",
|
||||||
|
"binary_sensor.technove_station_charging",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("init_integration")
|
@pytest.mark.usefixtures("init_integration")
|
||||||
async def test_disabled_by_default_binary_sensors(
|
async def test_disabled_by_default_binary_sensors(
|
||||||
|
@ -64,9 +67,9 @@ async def test_binary_sensor_update_failure(
|
||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test coordinator update failure."""
|
"""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")
|
mock_technove.update.side_effect = TechnoVEError("Test error")
|
||||||
freezer.tick(timedelta(minutes=5, seconds=1))
|
freezer.tick(timedelta(minutes=5, seconds=1))
|
||||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
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 homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from . import setup_with_selected_platforms
|
from . import setup_with_selected_platforms
|
||||||
|
@ -53,6 +53,12 @@ async def test_switches(
|
||||||
{"enabled": True},
|
{"enabled": True},
|
||||||
{"enabled": False},
|
{"enabled": False},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"switch.technove_station_charging_enabled",
|
||||||
|
"set_charging_enabled",
|
||||||
|
{"enabled": True},
|
||||||
|
{"enabled": False},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("init_integration")
|
@pytest.mark.usefixtures("init_integration")
|
||||||
|
@ -96,6 +102,10 @@ async def test_switch_on_off(
|
||||||
"switch.technove_station_auto_charge",
|
"switch.technove_station_auto_charge",
|
||||||
"set_auto_charge",
|
"set_auto_charge",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"switch.technove_station_charging_enabled",
|
||||||
|
"set_charging_enabled",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("init_integration")
|
@pytest.mark.usefixtures("init_integration")
|
||||||
|
@ -130,6 +140,10 @@ async def test_invalid_response(
|
||||||
"switch.technove_station_auto_charge",
|
"switch.technove_station_auto_charge",
|
||||||
"set_auto_charge",
|
"set_auto_charge",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"switch.technove_station_charging_enabled",
|
||||||
|
"set_charging_enabled",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("init_integration")
|
@pytest.mark.usefixtures("init_integration")
|
||||||
|
@ -157,3 +171,31 @@ async def test_connection_error(
|
||||||
assert method_mock.call_count == 1
|
assert method_mock.call_count == 1
|
||||||
assert (state := hass.states.get(state.entity_id))
|
assert (state := hass.states.get(state.entity_id))
|
||||||
assert state.state == STATE_UNAVAILABLE
|
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