mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Add charging mode select to Silla Prism (#181424)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user