mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 17:31:15 -04:00
Read the Sofar meter counters from meter_energy (#181729)
This commit is contained in:
@@ -1153,7 +1153,7 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
|
||||
),
|
||||
SofarSensorDescription(
|
||||
key="load_consumption_today",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_today",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1163,7 +1163,7 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
|
||||
),
|
||||
SofarSensorDescription(
|
||||
key="load_consumption_total",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1172,7 +1172,7 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
|
||||
),
|
||||
SofarSensorDescription(
|
||||
key="import_energy_today",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="import_energy_today",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1182,7 +1182,7 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
|
||||
),
|
||||
SofarSensorDescription(
|
||||
key="import_energy_total",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="import_energy_total",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1191,7 +1191,7 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
|
||||
),
|
||||
SofarSensorDescription(
|
||||
key="export_energy_today",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="export_energy_today",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1201,7 +1201,7 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
|
||||
),
|
||||
SofarSensorDescription(
|
||||
key="export_energy_total",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="export_energy_total",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
|
||||
@@ -50,6 +50,19 @@ def seed_pv_inverter(unit: MockModbusUnit, serial: str = MOCK_SERIAL) -> None:
|
||||
unit.holding[0x0685] = 1000 # solar_generation_today low word -> 10.0 kWh
|
||||
|
||||
|
||||
def deny_meter_energy(unit: MockModbusUnit) -> None:
|
||||
"""Seed a model that denies the meter block from 0x0688 on."""
|
||||
# Bits 0-3 must stay set, or the mask is ignored and denies nothing.
|
||||
unit.holding[0x0683] = 0x00FF
|
||||
|
||||
|
||||
def serve_meter_energy(unit: MockModbusUnit) -> None:
|
||||
"""Seed a model that serves the whole meter block."""
|
||||
unit.holding[0x0683] = 0xFFFF # 0x0680-0x068F
|
||||
unit.holding[0x0682] = 0x000F # 0x0690-0x0693
|
||||
unit.holding[0x068B] = 10000 # load_consumption_total -> 1000.0 kWh
|
||||
|
||||
|
||||
def seed_hybrid_inverter(
|
||||
unit: MockModbusUnit, serial: str = MOCK_HYBRID_SERIAL
|
||||
) -> None:
|
||||
|
||||
@@ -29,7 +29,9 @@ from . import (
|
||||
MOCK_SERIAL,
|
||||
MOCK_SW_VERSION,
|
||||
MOCK_USER_INPUT,
|
||||
deny_meter_energy,
|
||||
seed_hybrid_inverter,
|
||||
serve_meter_energy,
|
||||
)
|
||||
|
||||
from tests.common import (
|
||||
@@ -178,6 +180,108 @@ async def test_setup_skips_seeding_an_unusable_restored_total(
|
||||
mock_seed.assert_not_called()
|
||||
|
||||
|
||||
METER_ENERGY_KEYS = (
|
||||
"load_consumption_today",
|
||||
"load_consumption_total",
|
||||
"import_energy_today",
|
||||
"import_energy_total",
|
||||
"export_energy_today",
|
||||
"export_energy_total",
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_creates_no_meter_sensors_a_model_denies(
|
||||
hass: HomeAssistant,
|
||||
mock_connection: MockModbusConnection,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test an unmetered model publishes none of the meter sensors."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
deny_meter_energy(mock_connection.for_unit(1))
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sofar.async_get_unit",
|
||||
side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit(
|
||||
unit_id
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert not any(
|
||||
entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_{key}"
|
||||
)
|
||||
for key in METER_ENERGY_KEYS
|
||||
)
|
||||
# Solar generation shares the block but its own addresses stay valid.
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_solar_generation_total"
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_keeps_meter_sensors_a_model_serves(
|
||||
hass: HomeAssistant,
|
||||
mock_connection: MockModbusConnection,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test a metered model keeps all six sensors and their real values."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
serve_meter_energy(mock_connection.for_unit(1))
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sofar.async_get_unit",
|
||||
side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit(
|
||||
unit_id
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert all(
|
||||
entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_{key}"
|
||||
)
|
||||
for key in METER_ENERGY_KEYS
|
||||
)
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_load_consumption_total"
|
||||
)
|
||||
assert hass.states.get(entity_id).state == "1000.0"
|
||||
|
||||
|
||||
async def test_setup_keeps_meter_sensors_when_no_mask_is_published(
|
||||
hass: HomeAssistant,
|
||||
mock_connection: MockModbusConnection,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test a model publishing no usable mask keeps polling the meter block."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sofar.async_get_unit",
|
||||
side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit(
|
||||
unit_id
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert all(
|
||||
entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_{key}"
|
||||
)
|
||||
is not None
|
||||
for key in METER_ENERGY_KEYS
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_entry_unrecognized_inverter_raises_setup_error(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
|
||||
@@ -261,11 +261,11 @@ async def test_total_sensor_restore_data_parsing(
|
||||
device = runtime_data.readings.device
|
||||
description = SofarSensorDescription(
|
||||
key="load_consumption_total",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
)
|
||||
|
||||
device.energy.load_consumption_total = None
|
||||
device.meter_energy.load_consumption_total = None
|
||||
sensor = SofarTotalSensor(runtime_data, description)
|
||||
sensor.hass = hass
|
||||
sensor.async_get_last_sensor_data = AsyncMock(
|
||||
@@ -290,11 +290,11 @@ async def test_total_sensor_restore_data_parsing(
|
||||
await blank_sensor.async_added_to_hass()
|
||||
assert blank_sensor.native_value is None
|
||||
|
||||
device.energy.load_consumption_total = 120.0
|
||||
device.meter_energy.load_consumption_total = 120.0
|
||||
total_sensor = SofarTotalSensor(runtime_data, description)
|
||||
assert total_sensor.native_value == 120.0
|
||||
|
||||
device.energy.load_consumption_total = None
|
||||
device.meter_energy.load_consumption_total = None
|
||||
unset_sensor = SofarTotalSensor(runtime_data, description)
|
||||
assert unset_sensor.native_value is None
|
||||
|
||||
@@ -307,7 +307,7 @@ async def test_total_sensor_seeds_high_water_from_restored_value(
|
||||
device = runtime_data.readings.device
|
||||
description = SofarSensorDescription(
|
||||
key="load_consumption_total",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
@@ -316,7 +316,7 @@ async def test_total_sensor_seeds_high_water_from_restored_value(
|
||||
sensor.async_get_last_sensor_data = AsyncMock(
|
||||
return_value=SimpleNamespace(native_value="555.5")
|
||||
)
|
||||
with patch.object(device.energy, "seed_high_water") as mock_seed:
|
||||
with patch.object(device.meter_energy, "seed_high_water") as mock_seed:
|
||||
await sensor.async_added_to_hass()
|
||||
mock_seed.assert_called_once_with("load_consumption_total", 555.5)
|
||||
|
||||
@@ -342,7 +342,7 @@ async def test_total_sensor_dead_link_unavailable(
|
||||
runtime_data = init_integration.runtime_data
|
||||
description = SofarSensorDescription(
|
||||
key="load_consumption_total",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
@@ -390,13 +390,15 @@ async def test_total_sensor_total_increasing_uses_corrected_value(
|
||||
runtime_data = init_integration.runtime_data
|
||||
description = SofarSensorDescription(
|
||||
key="load_consumption_total",
|
||||
component="energy",
|
||||
component="meter_energy",
|
||||
translation_key="load_consumption_total",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
device = runtime_data.readings.device
|
||||
sensor = SofarTotalSensor(runtime_data, description)
|
||||
with patch.object(device.energy, "corrected", return_value=42.0) as mock_corrected:
|
||||
with patch.object(
|
||||
device.meter_energy, "corrected", return_value=42.0
|
||||
) as mock_corrected:
|
||||
assert sensor.native_value == 42.0
|
||||
mock_corrected.assert_called_once_with("load_consumption_total")
|
||||
assert sensor.available
|
||||
|
||||
Reference in New Issue
Block a user