mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 14:29:21 -04:00
Replace getattr lookups in Sofar with typed accessors (#183295)
This commit is contained in:
@@ -12,7 +12,6 @@ from homeassistant.components.modbus import async_get_unit
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorExtraStoredData,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
@@ -104,7 +103,7 @@ def _async_seed_high_water_marks(
|
||||
registry = er.async_get(hass)
|
||||
last_states = restore_state.async_get(hass).last_states
|
||||
for description in SENSOR_DESCRIPTIONS:
|
||||
if description.state_class is not SensorStateClass.TOTAL_INCREASING:
|
||||
if (total_fn := description.total_fn) is None:
|
||||
continue
|
||||
entity_id = registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{serial}_{description.key}"
|
||||
@@ -116,9 +115,7 @@ def _async_seed_high_water_marks(
|
||||
extra = SensorExtraStoredData.from_dict(stored.extra_data.as_dict())
|
||||
if extra is None or not isinstance(extra.native_value, (int, float)):
|
||||
continue
|
||||
getattr(device, description.component).seed_high_water(
|
||||
description.key, float(extra.native_value)
|
||||
)
|
||||
total_fn(device).seed(float(extra.native_value))
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Support for Sofar binary sensors."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import IntFlag
|
||||
from typing import override
|
||||
|
||||
from sofar_modbus.modern.device import SofarInverter
|
||||
from sofar_modbus.modern.enums import PowerControlFlags
|
||||
from sofar_modbus.modern.faults import FaultCategory
|
||||
|
||||
@@ -60,7 +62,7 @@ class SofarFlagBinarySensorDescription(
|
||||
):
|
||||
"""Describe a Sofar binary sensor backed by one flags-register bit."""
|
||||
|
||||
attribute: str
|
||||
flags_fn: Callable[[SofarInverter], IntFlag | None]
|
||||
flag: IntFlag
|
||||
|
||||
|
||||
@@ -71,7 +73,7 @@ FLAG_SENSOR_DESCRIPTIONS: tuple[SofarFlagBinarySensorDescription, ...] = (
|
||||
translation_key="active_power_limit_enabled",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
attribute="power_control",
|
||||
flags_fn=lambda device: device.active_power_control.power_control,
|
||||
flag=PowerControlFlags.ACTIVE_POWER,
|
||||
),
|
||||
)
|
||||
@@ -105,10 +107,9 @@ class SofarFaultBinarySensor(SofarEntity, BinarySensorEntity):
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
component = getattr(self.coordinator.device, self.entity_description.component)
|
||||
return any(
|
||||
fault.category is self.entity_description.category
|
||||
for fault in component.active_faults
|
||||
for fault in self.coordinator.device.state.active_faults
|
||||
)
|
||||
|
||||
|
||||
@@ -119,7 +120,6 @@ class SofarFlagBinarySensor(SofarEntity, BinarySensorEntity):
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
component = getattr(self.coordinator.device, self.entity_description.component)
|
||||
flags = getattr(component, self.entity_description.attribute)
|
||||
return self.entity_description.flag in flags
|
||||
def is_on(self) -> bool | None:
|
||||
flags = self.entity_description.flags_fn(self.coordinator.device)
|
||||
return None if flags is None else self.entity_description.flag in flags
|
||||
|
||||
@@ -179,8 +179,7 @@ class SofarRuntimeData:
|
||||
component_name = BATTERY_COMPONENTS[number]
|
||||
if component_name not in self.served_components:
|
||||
return False
|
||||
component = getattr(self.readings.device, component_name)
|
||||
return bool(getattr(component, f"battery_voltage_{number}", None))
|
||||
return bool(self.readings.device.battery_string(number).voltage)
|
||||
|
||||
def coordinator_for(self, component: str) -> SofarDataUpdateCoordinator:
|
||||
"""Which coordinator owns a given component's data."""
|
||||
|
||||
@@ -28,6 +28,7 @@ class SofarSelectEntityDescription(SelectEntityDescription, SofarEntityDescripti
|
||||
"""Describe a Sofar select entity."""
|
||||
|
||||
options_enum: type[IntEnum]
|
||||
value_fn: Callable[[SofarInverter], IntEnum | None]
|
||||
write_fn: Callable[[SofarInverter, int], Awaitable[None]]
|
||||
|
||||
|
||||
@@ -38,6 +39,7 @@ SELECT_DESCRIPTIONS: tuple[SofarSelectEntityDescription, ...] = (
|
||||
translation_key="charger_use_mode",
|
||||
options=_enum_options(ChargerUseMode),
|
||||
options_enum=ChargerUseMode,
|
||||
value_fn=lambda device: device.charger.charger_use_mode,
|
||||
write_fn=lambda device, value: device.charger.write("charger_use_mode", value),
|
||||
),
|
||||
SofarSelectEntityDescription(
|
||||
@@ -46,6 +48,7 @@ SELECT_DESCRIPTIONS: tuple[SofarSelectEntityDescription, ...] = (
|
||||
translation_key="eps_control",
|
||||
options=_enum_options(EpsControlMode),
|
||||
options_enum=EpsControlMode,
|
||||
value_fn=lambda device: device.eps.eps_control,
|
||||
write_fn=lambda device, value: device.eps.async_write_control(
|
||||
EpsControlMode(value)
|
||||
),
|
||||
@@ -77,8 +80,7 @@ class SofarSelect(SofarEntity, SelectEntity):
|
||||
@override
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the currently selected option."""
|
||||
component = getattr(self.coordinator.device, self.entity_description.component)
|
||||
value: IntEnum | None = getattr(component, self.entity_description.key)
|
||||
value = self.entity_description.value_fn(self.coordinator.device)
|
||||
return value.name.lower() if value is not None else None
|
||||
|
||||
@override
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ PARALLEL_UPDATES = 1
|
||||
class SofarSwitchEntityDescription(SwitchEntityDescription, SofarEntityDescription):
|
||||
"""Describe a Sofar switch entity."""
|
||||
|
||||
value_fn: Callable[[SofarInverter], RemoteSwitchOnOff | None]
|
||||
write_fn: Callable[[SofarInverter, bool], Awaitable[None]]
|
||||
|
||||
|
||||
@@ -29,6 +30,7 @@ SWITCH_DESCRIPTIONS: tuple[SofarSwitchEntityDescription, ...] = (
|
||||
key="remote_switch_on_off",
|
||||
component="remote",
|
||||
name=None,
|
||||
value_fn=lambda device: device.remote.remote_switch_on_off,
|
||||
write_fn=lambda device, value: device.remote.write(
|
||||
"remote_switch_on_off",
|
||||
RemoteSwitchOnOff.ON if value else RemoteSwitchOnOff.OFF,
|
||||
@@ -61,8 +63,7 @@ class SofarSwitch(SofarEntity, SwitchEntity):
|
||||
@override
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return whether the remote switch is on."""
|
||||
component = getattr(self.coordinator.device, self.entity_description.component)
|
||||
value = getattr(component, self.entity_description.key)
|
||||
value = self.entity_description.value_fn(self.coordinator.device)
|
||||
return None if value is None else bool(value)
|
||||
|
||||
async def _async_write(self, value: bool) -> None:
|
||||
|
||||
@@ -265,6 +265,7 @@ async def test_total_sensor_restore_data_parsing(
|
||||
key="load_consumption_total",
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
value_fn=lambda device: device.meter_energy.load_consumption_total,
|
||||
)
|
||||
|
||||
device.meter_energy.load_consumption_total = None
|
||||
@@ -312,6 +313,10 @@ async def test_total_sensor_seeds_high_water_from_restored_value(
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: (
|
||||
device.meter_energy.load_consumption_total_corrected.value
|
||||
),
|
||||
total_fn=lambda device: device.meter_energy.load_consumption_total_corrected,
|
||||
)
|
||||
sensor = SofarTotalSensor(runtime_data, description)
|
||||
sensor.hass = hass
|
||||
@@ -330,6 +335,7 @@ async def test_sensor_dead_link_unavailable(init_integration: MockConfigEntry) -
|
||||
key="grid_frequency",
|
||||
component="grid",
|
||||
translation_key="grid_frequency",
|
||||
value_fn=lambda device: device.grid.grid_frequency,
|
||||
)
|
||||
sensor = SofarSensor(runtime_data, description)
|
||||
assert sensor.native_value == 50.0
|
||||
@@ -347,6 +353,10 @@ async def test_total_sensor_dead_link_unavailable(
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: (
|
||||
device.meter_energy.load_consumption_total_corrected.value
|
||||
),
|
||||
total_fn=lambda device: device.meter_energy.load_consumption_total_corrected,
|
||||
)
|
||||
sensor = SofarTotalSensor(runtime_data, description)
|
||||
assert sensor.available
|
||||
@@ -367,6 +377,7 @@ async def test_sensor_availability_on_component_failure(
|
||||
key="grid_frequency",
|
||||
component="grid",
|
||||
translation_key="grid_frequency",
|
||||
value_fn=lambda device: device.grid.grid_frequency,
|
||||
)
|
||||
sensor = SofarSensor(runtime_data, description)
|
||||
assert sensor.available
|
||||
@@ -395,6 +406,10 @@ async def test_total_sensor_total_increasing_uses_corrected_value(
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: (
|
||||
device.meter_energy.load_consumption_total_corrected.value
|
||||
),
|
||||
total_fn=lambda device: device.meter_energy.load_consumption_total_corrected,
|
||||
)
|
||||
device = runtime_data.readings.device
|
||||
sensor = SofarTotalSensor(runtime_data, description)
|
||||
@@ -404,3 +419,43 @@ async def test_total_sensor_total_increasing_uses_corrected_value(
|
||||
assert sensor.native_value == 42.0
|
||||
mock_corrected.assert_called_once_with("load_consumption_total")
|
||||
assert sensor.available
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"description",
|
||||
[
|
||||
pytest.param(description, id=description.key)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.state_class is not SensorStateClass.TOTAL_INCREASING
|
||||
],
|
||||
)
|
||||
async def test_value_fn_reads_the_field_its_key_names(
|
||||
init_integration: MockConfigEntry, description: SofarSensorDescription
|
||||
) -> None:
|
||||
"""Test each value_fn reads the field its key and component name."""
|
||||
device = init_integration.runtime_data.readings.device
|
||||
component = getattr(device, description.component)
|
||||
sentinel = object()
|
||||
with patch.dict(component._values, {description.key: sentinel}):
|
||||
assert description.value_fn(device) is sentinel
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"description",
|
||||
[
|
||||
pytest.param(description, id=description.key)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.state_class is SensorStateClass.TOTAL_INCREASING
|
||||
],
|
||||
)
|
||||
async def test_total_reads_and_seeds_its_own_corrected_total(
|
||||
init_integration: MockConfigEntry, description: SofarSensorDescription
|
||||
) -> None:
|
||||
"""Test each total reads and seeds the corrected total its key names."""
|
||||
device = init_integration.runtime_data.readings.device
|
||||
component = getattr(device, description.component)
|
||||
with patch.object(component, "corrected", return_value=42.0) as mock_corrected:
|
||||
assert description.value_fn(device) == 42.0
|
||||
mock_corrected.assert_called_once_with(description.key)
|
||||
assert description.total_fn is not None
|
||||
assert description.total_fn(device).name == description.key
|
||||
|
||||
Reference in New Issue
Block a user