Add charging mode select to Silla Prism (#181424)

This commit is contained in:
Ermanno Baschiera
2026-09-10 21:07:44 +02:00
committed by GitHub
parent e59059d63b
commit 9485e2cfee
8 changed files with 245 additions and 8 deletions
@@ -6,7 +6,7 @@ from homeassistant.const import Platform
DOMAIN: Final = "silla_prism"
PLATFORMS: Final = [Platform.SENSOR]
PLATFORMS: Final = [Platform.SELECT, Platform.SENSOR]
CONF_BASE_TOPIC: Final = "base_topic"
@@ -39,7 +39,7 @@ class PrismCoordinator(DataUpdateCoordinator[PrismStatus]):
"""Initialize the coordinator."""
super().__init__(hass, _LOGGER, config_entry=entry, name=DOMAIN)
self.base_topic: str = entry.data[CONF_BASE_TOPIC]
self.device = PrismDevice(self.base_topic)
self.device = PrismDevice(self.base_topic, publish=self._async_publish)
self.device.on_status_update = self._on_status_update
self.device.on_hello = self._on_hello
@@ -60,6 +60,10 @@ class PrismCoordinator(DataUpdateCoordinator[PrismStatus]):
"""Return the accumulated state (populated by retained messages)."""
return self.device.status
async def _async_publish(self, topic: str, payload: str) -> None:
"""Publish a command built by the library."""
await mqtt.async_publish(self.hass, topic, payload)
@callback
def _message_received(self, msg: ReceiveMessage) -> None:
if isinstance(msg.payload, str):
@@ -0,0 +1,54 @@
"""Select platform for the Silla Prism integration."""
from typing import override
from pysillaprism import SETTABLE_MODES
from homeassistant.components.select import SelectEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import PORT
from .coordinator import PrismConfigEntry, PrismCoordinator
from .entity import PrismEntity
PARALLEL_UPDATES = 0
OPTION_TO_MODE = {mode.name.lower(): mode for mode in SETTABLE_MODES}
MODE_TO_OPTION = {mode: option for option, mode in OPTION_TO_MODE.items()}
async def async_setup_entry(
hass: HomeAssistant,
entry: PrismConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Prism select entities."""
async_add_entities([PrismModeSelect(entry.runtime_data)])
class PrismModeSelect(PrismEntity, SelectEntity):
"""Charging mode selector (Solar/Normal/Pause)."""
_attr_translation_key = "charging_mode"
_attr_options = list(OPTION_TO_MODE)
def __init__(self, coordinator: PrismCoordinator) -> None:
"""Initialize the select entity."""
super().__init__(coordinator, "charging_mode")
@property
@override
def current_option(self) -> str | None:
"""Return the current mode, or None when it is not user-settable.
Prism also reports a load-balancing pause, which cannot be selected
back; it surfaces on the status sensor instead.
"""
mode = self.coordinator.device.status.port(PORT).mode
return MODE_TO_OPTION.get(mode) if mode is not None else None
@override
async def async_select_option(self, option: str) -> None:
"""Change the charging mode."""
await self.coordinator.device.set_mode(OPTION_TO_MODE[option], PORT)
@@ -28,6 +28,16 @@
}
},
"entity": {
"select": {
"charging_mode": {
"name": "Charging mode",
"state": {
"normal": "[%key:common::state::normal%]",
"pause": "[%key:common::state::paused%]",
"solar": "Solar"
}
}
},
"sensor": {
"error": {
"name": "Error",
+15 -4
View File
@@ -1,5 +1,9 @@
"""Tests for the Silla Prism integration."""
from unittest.mock import patch
from homeassistant.components.silla_prism.const import PLATFORMS
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import RETAINED_BURST
@@ -7,10 +11,17 @@ from .const import RETAINED_BURST
from tests.common import MockConfigEntry, async_fire_mqtt_message
async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
"""Set up the Silla Prism integration."""
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
async def setup_integration(
hass: HomeAssistant,
entry: MockConfigEntry,
platforms: list[Platform] | None = None,
) -> None:
"""Set up the Silla Prism integration, optionally limited to some platforms."""
with patch(
"homeassistant.components.silla_prism.PLATFORMS", platforms or PLATFORMS
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
async def fire_burst(hass: HomeAssistant) -> None:
@@ -0,0 +1,62 @@
# serializer version: 1
# name: test_selects[select.silla_prism_charging_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'solar',
'normal',
'pause',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.silla_prism_charging_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Charging mode',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Charging mode',
'platform': 'silla_prism',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charging_mode',
'unique_id': 'prism_charging_mode',
'unit_of_measurement': None,
})
# ---
# name: test_selects[select.silla_prism_charging_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Silla Prism Charging mode',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'solar',
'normal',
'pause',
]),
}),
'context': <ANY>,
'entity_id': 'select.silla_prism_charging_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'normal',
})
# ---
@@ -0,0 +1,96 @@
"""Test the Silla Prism selects."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, 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
CHARGING_MODE_ENTITY_ID = "select.silla_prism_charging_mode"
MODE_TOPIC = "prism/1/mode"
SET_MODE_TOPIC = "prism/1/command/set_mode"
async def test_selects(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the selects."""
await setup_integration(hass, mock_config_entry, [Platform.SELECT])
await fire_burst(hass)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("option", "payload"),
[("solar", "1"), ("normal", "2"), ("pause", "3")],
)
async def test_select_option(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
mock_config_entry: MockConfigEntry,
option: str,
payload: str,
) -> None:
"""Test that selecting a mode publishes the matching command."""
await setup_integration(hass, mock_config_entry)
await fire_burst(hass)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: CHARGING_MODE_ENTITY_ID, ATTR_OPTION: option},
blocking=True,
)
mqtt_mock.async_publish.assert_called_once_with(
SET_MODE_TOPIC, payload, 0, False, message_expiry_interval=None
)
@pytest.mark.usefixtures("mqtt_mock")
async def test_mode_updates(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that the selected mode follows the reported one."""
await setup_integration(hass, mock_config_entry)
await fire_burst(hass)
assert hass.states.get(CHARGING_MODE_ENTITY_ID).state == "normal"
async_fire_mqtt_message(hass, MODE_TOPIC, "1")
await hass.async_block_till_done()
assert hass.states.get(CHARGING_MODE_ENTITY_ID).state == "solar"
@pytest.mark.usefixtures("mqtt_mock")
async def test_mode_not_selectable(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a mode Prism cannot be put back into reads as unknown."""
await setup_integration(hass, mock_config_entry)
await fire_burst(hass)
# Load balancing suspended the session: reported, but not user-settable.
async_fire_mqtt_message(hass, MODE_TOPIC, "7")
await hass.async_block_till_done()
assert hass.states.get(CHARGING_MODE_ENTITY_ID).state == STATE_UNKNOWN
+2 -2
View File
@@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import STATE_UNKNOWN
from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -33,7 +33,7 @@ async def test_sensors(
) -> None:
"""Test the sensors."""
freezer.move_to("2026-01-01 00:00:00+00:00")
await setup_integration(hass, mock_config_entry)
await setup_integration(hass, mock_config_entry, [Platform.SENSOR])
await fire_burst(hass)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)