diff --git a/homeassistant/components/silla_prism/const.py b/homeassistant/components/silla_prism/const.py index 07f18a1ad251..cf07ab4a674f 100644 --- a/homeassistant/components/silla_prism/const.py +++ b/homeassistant/components/silla_prism/const.py @@ -6,7 +6,7 @@ from homeassistant.const import Platform DOMAIN: Final = "silla_prism" -PLATFORMS: Final = [Platform.SELECT, Platform.SENSOR] +PLATFORMS: Final = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR] CONF_BASE_TOPIC: Final = "base_topic" @@ -18,3 +18,7 @@ MODEL: Final = "Prism" #: Charging port targeted by this integration. Single-cable Prisms only expose #: port 1; DUO support (port 2) is intentionally left for a follow-up. PORT: Final = 1 + +#: Minimum/maximum charging current, in amps, as accepted by Prism. +MIN_CURRENT: Final = 6 +MAX_CURRENT: Final = 32 diff --git a/homeassistant/components/silla_prism/number.py b/homeassistant/components/silla_prism/number.py new file mode 100644 index 000000000000..45b93827bff5 --- /dev/null +++ b/homeassistant/components/silla_prism/number.py @@ -0,0 +1,50 @@ +"""Number platform for the Silla Prism integration.""" + +from typing import override + +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode +from homeassistant.const import UnitOfElectricCurrent +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import MAX_CURRENT, MIN_CURRENT, PORT +from .coordinator import PrismConfigEntry, PrismCoordinator +from .entity import PrismEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PrismConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Prism number entities.""" + async_add_entities([PrismUserCurrentNumber(entry.runtime_data)]) + + +class PrismUserCurrentNumber(PrismEntity, NumberEntity): + """User-set maximum charging current (mirrors the +/- buttons).""" + + _attr_translation_key = "user_current" + _attr_device_class = NumberDeviceClass.CURRENT + _attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE + _attr_native_min_value = MIN_CURRENT + _attr_native_max_value = MAX_CURRENT + _attr_native_step = 1 + _attr_mode = NumberMode.SLIDER + + def __init__(self, coordinator: PrismCoordinator) -> None: + """Initialize the number entity.""" + super().__init__(coordinator, "user_current") + + @property + @override + def native_value(self) -> float | None: + """Return the user-set current reported by Prism, in amps.""" + return self.coordinator.device.status.port(PORT).user_current + + @override + async def async_set_native_value(self, value: float) -> None: + """Set the user maximum charging current.""" + await self.coordinator.device.set_current_user(int(value), PORT) diff --git a/homeassistant/components/silla_prism/strings.json b/homeassistant/components/silla_prism/strings.json index 503268a742aa..736531fa1765 100644 --- a/homeassistant/components/silla_prism/strings.json +++ b/homeassistant/components/silla_prism/strings.json @@ -28,6 +28,11 @@ } }, "entity": { + "number": { + "user_current": { + "name": "Maximum charging current" + } + }, "select": { "charging_mode": { "name": "Charging mode", diff --git a/tests/components/silla_prism/snapshots/test_number.ambr b/tests/components/silla_prism/snapshots/test_number.ambr new file mode 100644 index 000000000000..0653f1457dac --- /dev/null +++ b/tests/components/silla_prism/snapshots/test_number.ambr @@ -0,0 +1,62 @@ +# serializer version: 1 +# name: test_numbers[number.silla_prism_maximum_charging_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 32, + : 6, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.silla_prism_maximum_charging_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Maximum charging current', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum charging current', + 'platform': 'silla_prism', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'user_current', + 'unique_id': 'prism_user_current', + 'unit_of_measurement': , + }) +# --- +# name: test_numbers[number.silla_prism_maximum_charging_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Silla Prism Maximum charging current', + : 32, + : 6, + : , + : 1, + : , + }), + 'context': , + 'entity_id': 'number.silla_prism_maximum_charging_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- diff --git a/tests/components/silla_prism/test_number.py b/tests/components/silla_prism/test_number.py new file mode 100644 index 000000000000..0c7ff16002a9 --- /dev/null +++ b/tests/components/silla_prism/test_number.py @@ -0,0 +1,74 @@ +"""Test the Silla Prism numbers.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import fire_burst, setup_integration + +from tests.common import MockConfigEntry, async_fire_mqtt_message, snapshot_platform +from tests.typing import MqttMockHAClient + +USER_CURRENT_ENTITY_ID = "number.silla_prism_maximum_charging_current" +USER_AMP_TOPIC = "prism/1/user_amp" +SET_CURRENT_USER_TOPIC = "prism/1/command/set_current_user" + + +async def test_numbers( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the numbers.""" + await setup_integration(hass, mock_config_entry, [Platform.NUMBER]) + await fire_burst(hass) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_set_user_current( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that setting the current publishes the matching command.""" + await setup_integration(hass, mock_config_entry) + await fire_burst(hass) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: USER_CURRENT_ENTITY_ID, ATTR_VALUE: 16}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + SET_CURRENT_USER_TOPIC, "16", 0, False, message_expiry_interval=None + ) + + +@pytest.mark.usefixtures("mqtt_mock") +async def test_user_current_updates( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the current follows the one reported by Prism.""" + await setup_integration(hass, mock_config_entry) + await fire_burst(hass) + + assert hass.states.get(USER_CURRENT_ENTITY_ID).state == "6" + + async_fire_mqtt_message(hass, USER_AMP_TOPIC, "20") + await hass.async_block_till_done() + + assert hass.states.get(USER_CURRENT_ENTITY_ID).state == "20"