diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index f3db59a65ae..e7fb41b0788 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -21,6 +21,7 @@ PLATFORMS = [ Platform.COVER, Platform.DEVICE_TRACKER, Platform.LOCK, + Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/tessie/number.py b/homeassistant/components/tessie/number.py new file mode 100644 index 00000000000..204260a7ab6 --- /dev/null +++ b/homeassistant/components/tessie/number.py @@ -0,0 +1,137 @@ +"""Number platform for Tessie integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from tessie_api import set_charge_limit, set_charging_amps, set_speed_limit + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + PRECISION_WHOLE, + UnitOfElectricCurrent, + UnitOfSpeed, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import TessieDataUpdateCoordinator +from .entity import TessieEntity + + +@dataclass(frozen=True, kw_only=True) +class TessieNumberEntityDescription(NumberEntityDescription): + """Describes Tessie Number entity.""" + + func: Callable + arg: str + native_min_value: float + native_max_value: float + min_key: str | None = None + max_key: str + + +DESCRIPTIONS: tuple[TessieNumberEntityDescription, ...] = ( + TessieNumberEntityDescription( + key="charge_state_charge_current_request", + native_step=PRECISION_WHOLE, + native_min_value=0, + native_max_value=32, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=NumberDeviceClass.CURRENT, + max_key="charge_state_charge_current_request_max", + func=set_charging_amps, + arg="amps", + ), + TessieNumberEntityDescription( + key="charge_state_charge_limit_soc", + native_step=PRECISION_WHOLE, + native_min_value=50, + native_max_value=100, + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + min_key="charge_state_charge_limit_soc_min", + max_key="charge_state_charge_limit_soc_max", + func=set_charge_limit, + arg="percent", + ), + TessieNumberEntityDescription( + key="vehicle_state_speed_limit_mode_current_limit_mph", + native_step=PRECISION_WHOLE, + native_min_value=50, + native_max_value=120, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=NumberDeviceClass.SPEED, + mode=NumberMode.BOX, + min_key="vehicle_state_speed_limit_mode_min_limit_mph", + max_key="vehicle_state_speed_limit_mode_max_limit_mph", + func=set_speed_limit, + arg="mph", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tessie sensor platform from a config entry.""" + coordinators = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + TessieNumberEntity(coordinator, description) + for coordinator in coordinators + for description in DESCRIPTIONS + if description.key in coordinator.data + ) + + +class TessieNumberEntity(TessieEntity, NumberEntity): + """Number entity for current charge.""" + + entity_description: TessieNumberEntityDescription + + def __init__( + self, + coordinator: TessieDataUpdateCoordinator, + description: TessieNumberEntityDescription, + ) -> None: + """Initialize the Number entity.""" + super().__init__(coordinator, description.key) + self.entity_description = description + + @property + def native_value(self) -> float | None: + """Return the value reported by the number.""" + return self._value + + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + if self.entity_description.min_key: + return self.get( + self.entity_description.min_key, + self.entity_description.native_min_value, + ) + return self.entity_description.native_min_value + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + return self.get( + self.entity_description.max_key, self.entity_description.native_max_value + ) + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self.run( + self.entity_description.func, **{self.entity_description.arg: value} + ) + self.set((self.key, value)) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 9bc6dfbd9bd..cb4b09ad3a4 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -273,6 +273,17 @@ "climate_state_steering_wheel_heater": { "name": "Steering wheel heater" } + }, + "number": { + "charge_state_charge_current_request": { + "name": "Charge current" + }, + "charge_state_charge_limit_soc": { + "name": "Charge limit" + }, + "vehicle_state_speed_limit_mode_current_limit_mph": { + "name": "Speed limit" + } } } } diff --git a/tests/components/tessie/test_number.py b/tests/components/tessie/test_number.py new file mode 100644 index 00000000000..f4a407f80c4 --- /dev/null +++ b/tests/components/tessie/test_number.py @@ -0,0 +1,70 @@ +"""Test the Tessie number platform.""" + + +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE +from homeassistant.components.tessie.number import DESCRIPTIONS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .common import TEST_VEHICLE_STATE_ONLINE, patch_description, setup_platform + + +async def test_numbers(hass: HomeAssistant) -> None: + """Tests that the number entities are correct.""" + + assert len(hass.states.async_all("number")) == 0 + + await setup_platform(hass) + + assert len(hass.states.async_all("number")) == len(DESCRIPTIONS) + + assert hass.states.get("number.test_charge_current").state == str( + TEST_VEHICLE_STATE_ONLINE["charge_state"]["charge_current_request"] + ) + + assert hass.states.get("number.test_charge_limit").state == str( + TEST_VEHICLE_STATE_ONLINE["charge_state"]["charge_limit_soc"] + ) + + assert hass.states.get("number.test_speed_limit").state == str( + TEST_VEHICLE_STATE_ONLINE["vehicle_state"]["speed_limit_mode"][ + "current_limit_mph" + ] + ) + + # Test number set value functions + with patch_description( + "charge_state_charge_current_request", "func", DESCRIPTIONS + ) as mock_set_charging_amps: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ["number.test_charge_current"], "value": 16}, + blocking=True, + ) + assert hass.states.get("number.test_charge_current").state == "16.0" + mock_set_charging_amps.assert_called_once() + + with patch_description( + "charge_state_charge_limit_soc", "func", DESCRIPTIONS + ) as mock_set_charge_limit: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ["number.test_charge_limit"], "value": 80}, + blocking=True, + ) + assert hass.states.get("number.test_charge_limit").state == "80.0" + mock_set_charge_limit.assert_called_once() + + with patch_description( + "vehicle_state_speed_limit_mode_current_limit_mph", "func", DESCRIPTIONS + ) as mock_set_speed_limit: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ["number.test_speed_limit"], "value": 60}, + blocking=True, + ) + assert hass.states.get("number.test_speed_limit").state == "60.0" + mock_set_speed_limit.assert_called_once()