mirror of
https://github.com/home-assistant/core.git
synced 2026-09-16 21:48:38 +01:00
378 lines
15 KiB
Python
378 lines
15 KiB
Python
"""Tests for the Tibber Data API sensors and coordinator."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from homeassistant.components.recorder import Recorder
|
|
from homeassistant.components.tibber.const import DOMAIN
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import (
|
|
area_registry as ar,
|
|
device_registry as dr,
|
|
entity_registry as er,
|
|
)
|
|
from homeassistant.helpers.entity_component import async_update_entity
|
|
|
|
from .conftest import create_tibber_device, create_tibber_home
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
|
|
async def test_price_sensor_state_unit_and_attributes(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
tibber_mock: MagicMock,
|
|
setup_credentials: None,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test price sensor state and attributes."""
|
|
home = create_tibber_home(current_price=1.25)
|
|
tibber_mock.get_homes.return_value = [home]
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, home.home_id)
|
|
assert entity_id is not None
|
|
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert float(state.state) == 1.25
|
|
assert state.attributes["unit_of_measurement"] == "NOK/kWh"
|
|
assert state.attributes["app_nickname"] == "Home"
|
|
assert state.attributes["grid_company"] == "GridCo"
|
|
assert state.attributes["estimated_annual_consumption"] == 12000
|
|
assert state.attributes["intraday_price_ranking"] == 0.4
|
|
assert state.attributes["max_price"] == 1.8
|
|
assert state.attributes["avg_price"] == 1.2
|
|
assert state.attributes["min_price"] == 0.8
|
|
assert state.attributes["off_peak_1"] == 0.9
|
|
assert state.attributes["peak"] == 1.7
|
|
assert state.attributes["off_peak_2"] == 1.0
|
|
|
|
await async_update_entity(hass, entity_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert float(state.state) == 1.25
|
|
assert state.attributes["unit_of_measurement"] == "NOK/kWh"
|
|
assert state.attributes["app_nickname"] == "Home"
|
|
assert state.attributes["grid_company"] == "GridCo"
|
|
assert state.attributes["estimated_annual_consumption"] == 12000
|
|
assert state.attributes["intraday_price_ranking"] == 0.4
|
|
assert state.attributes["max_price"] == 1.8
|
|
assert state.attributes["avg_price"] == 1.2
|
|
assert state.attributes["min_price"] == 0.8
|
|
assert state.attributes["off_peak_1"] == 0.9
|
|
assert state.attributes["peak"] == 1.7
|
|
assert state.attributes["off_peak_2"] == 1.0
|
|
|
|
|
|
@pytest.mark.usefixtures("recorder_mock", "setup_credentials")
|
|
async def test_data_api_sensors_migrate_to_device_id(
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
data_api_client_mock: AsyncMock,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
tibber_mock: MagicMock,
|
|
) -> None:
|
|
"""Test Data API sensors migrate to Tibber device IDs."""
|
|
home = create_tibber_home()
|
|
tibber_mock.get_homes.return_value = [home]
|
|
home_device = device_registry.async_get_or_create(
|
|
config_entry_id=config_entry.entry_id,
|
|
identifiers={(DOMAIN, home.home_id)},
|
|
)
|
|
legacy_device = device_registry.async_get_or_create(
|
|
config_entry_id=config_entry.entry_id,
|
|
identifiers={(DOMAIN, "external-id")},
|
|
name="Test Device",
|
|
)
|
|
orphaned_device = device_registry.async_get_or_create(
|
|
config_entry_id=config_entry.entry_id,
|
|
identifiers={(DOMAIN, "orphaned-external-id")},
|
|
)
|
|
stale_device = device_registry.async_get_or_create(
|
|
config_entry_id=config_entry.entry_id,
|
|
identifiers={(DOMAIN, "stale-device")},
|
|
)
|
|
legacy_entity = entity_registry.async_get_or_create(
|
|
"sensor",
|
|
DOMAIN,
|
|
"external-id_storage.stateOfCharge",
|
|
suggested_object_id="legacy_state_of_charge",
|
|
config_entry=config_entry,
|
|
device_id=legacy_device.id,
|
|
)
|
|
orphaned_tibber_device = create_tibber_device(
|
|
device_id="orphaned-device-id",
|
|
external_id="orphaned-external-id",
|
|
)
|
|
data_api_client_mock.get_all_devices = AsyncMock(
|
|
return_value={
|
|
"device-id": create_tibber_device(state_of_charge=72.0),
|
|
"orphaned-device-id": orphaned_tibber_device,
|
|
}
|
|
)
|
|
data_api_client_mock.update_devices = AsyncMock(
|
|
return_value={
|
|
"device-id": create_tibber_device(state_of_charge=83.0),
|
|
"orphaned-device-id": orphaned_tibber_device,
|
|
}
|
|
)
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
data_api_client_mock.get_all_devices.assert_awaited_once()
|
|
data_api_client_mock.update_devices.assert_awaited_once()
|
|
|
|
unique_id = "device-id_storage.stateOfCharge"
|
|
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)
|
|
assert entity_id == legacy_entity.entity_id
|
|
|
|
device = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, "device-id"), config_entry.entry_id
|
|
)
|
|
assert device is not None
|
|
assert device.id == legacy_device.id
|
|
migrated_orphaned_device = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, "orphaned-device-id"), config_entry.entry_id
|
|
)
|
|
assert migrated_orphaned_device is not None
|
|
assert migrated_orphaned_device.id == orphaned_device.id
|
|
assert device_registry.async_get(home_device.id) is not None
|
|
assert device_registry.async_get(stale_device.id) is None
|
|
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert float(state.state) == 83.0
|
|
|
|
|
|
@pytest.mark.usefixtures("recorder_mock", "setup_credentials")
|
|
@pytest.mark.parametrize(
|
|
"unsupported_device_name", ["Unsupported device", "Charger left"]
|
|
)
|
|
async def test_data_api_sensors_with_empty_external_ids(
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
data_api_client_mock: AsyncMock,
|
|
area_registry: ar.AreaRegistry,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
unsupported_device_name: str,
|
|
) -> None:
|
|
"""Test Data API sensors migrate empty external ID registry entries."""
|
|
area = area_registry.async_get_or_create("Outside")
|
|
legacy_device = device_registry.async_get_or_create(
|
|
config_entry_id=config_entry.entry_id,
|
|
identifiers={(DOMAIN, "")},
|
|
name="Charger left",
|
|
)
|
|
legacy_device = device_registry.async_update_device(
|
|
legacy_device.id, area_id=area.id
|
|
)
|
|
assert legacy_device is not None
|
|
|
|
legacy_entities = {
|
|
sensor_id: entity_registry.async_get_or_create(
|
|
"sensor",
|
|
DOMAIN,
|
|
f"_{sensor_id}",
|
|
suggested_object_id=f"legacy_{sensor_id}",
|
|
config_entry=config_entry,
|
|
device_id=legacy_device.id,
|
|
)
|
|
for sensor_id in (
|
|
"charging.current.max",
|
|
"charging.current.offlineFallback",
|
|
)
|
|
}
|
|
devices = {
|
|
"a-unsupported-device": create_tibber_device(
|
|
device_id="a-unsupported-device",
|
|
external_id="",
|
|
name=unsupported_device_name,
|
|
sensor_values={"unknown.sensor.id": None},
|
|
),
|
|
**{
|
|
device_id: create_tibber_device(
|
|
device_id=device_id,
|
|
external_id="",
|
|
name=name,
|
|
sensor_values={
|
|
"charging.current.max": 32.0,
|
|
"charging.current.offlineFallback": 16.0,
|
|
},
|
|
)
|
|
for device_id, name in (
|
|
("charger-right", "Charger right"),
|
|
("charger-left", "Charger left"),
|
|
)
|
|
},
|
|
}
|
|
data_api_client_mock.get_all_devices = AsyncMock(return_value=devices)
|
|
data_api_client_mock.update_devices = AsyncMock(return_value=devices)
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
left_entity_id = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, "charger-left_charging.current.max"
|
|
)
|
|
right_entity_id = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, "charger-right_charging.current.max"
|
|
)
|
|
assert left_entity_id is not None
|
|
assert right_entity_id is not None
|
|
assert left_entity_id == legacy_entities["charging.current.max"].entity_id
|
|
|
|
fallback_entity_id = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, "charger-left_charging.current.offlineFallback"
|
|
)
|
|
assert (
|
|
fallback_entity_id
|
|
== legacy_entities["charging.current.offlineFallback"].entity_id
|
|
)
|
|
|
|
left_entity = entity_registry.async_get(left_entity_id)
|
|
right_entity = entity_registry.async_get(right_entity_id)
|
|
assert left_entity is not None
|
|
assert right_entity is not None
|
|
assert left_entity.device_id != right_entity.device_id
|
|
|
|
left_device = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, "charger-left"), config_entry.entry_id
|
|
)
|
|
assert left_device is not None
|
|
assert left_device.id == legacy_device.id
|
|
assert left_device.area_id == area.id
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("sensor_id", "expected_value", "description"),
|
|
[
|
|
("storage.ratedCapacity", 10000.0, "Storage rated capacity"),
|
|
("storage.ratedPower", 5000.0, "Storage rated power"),
|
|
("storage.availableEnergy", 7500.0, "Storage available energy"),
|
|
("powerFlow.battery.power", 2500.0, "Battery power flow"),
|
|
("powerFlow.grid.power", 1500.0, "Grid power flow"),
|
|
("powerFlow.load.power", 4000.0, "Load power flow"),
|
|
("powerFlow.toGrid", 25.5, "Power flow to grid percentage"),
|
|
("powerFlow.toLoad", 60.0, "Power flow to load percentage"),
|
|
("powerFlow.fromGrid", 15.0, "Power flow from grid percentage"),
|
|
("powerFlow.fromLoad", 10.0, "Power flow from load percentage"),
|
|
("energyFlow.hour.battery.charged", 2000.0, "Hourly battery charged"),
|
|
("energyFlow.hour.battery.discharged", 1500.0, "Hourly battery discharged"),
|
|
("energyFlow.hour.grid.imported", 1000.0, "Hourly grid imported"),
|
|
("energyFlow.hour.grid.exported", 800.0, "Hourly grid exported"),
|
|
("energyFlow.hour.load.consumed", 3000.0, "Hourly load consumed"),
|
|
("energyFlow.hour.load.generated", 200.0, "Hourly load generated"),
|
|
("energyFlow.month.battery.charged", 50000.0, "Monthly battery charged"),
|
|
("energyFlow.month.battery.discharged", 40000.0, "Monthly battery discharged"),
|
|
("energyFlow.month.grid.imported", 25000.0, "Monthly grid imported"),
|
|
("energyFlow.month.grid.exported", 20000.0, "Monthly grid exported"),
|
|
("energyFlow.month.load.consumed", 60000.0, "Monthly load consumed"),
|
|
("energyFlow.month.load.generated", 5000.0, "Monthly load generated"),
|
|
("range.remaining", 250.5, "Remaining range"),
|
|
("charging.current.max", 32.0, "Max charging current"),
|
|
("charging.current.offlineFallback", 16.0, "Offline fallback charging current"),
|
|
("temp.setpoint", 22.5, "Temperature setpoint"),
|
|
("temp.current", 21.0, "Current temperature"),
|
|
("temp.comfort", 20.5, "Comfort temperature"),
|
|
("grid.phaseCount", 3.0, "Grid phase count"),
|
|
],
|
|
)
|
|
async def test_new_data_api_sensor_values(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
data_api_client_mock: AsyncMock,
|
|
setup_credentials: None,
|
|
entity_registry: er.EntityRegistry,
|
|
sensor_id: str,
|
|
expected_value: float,
|
|
description: str,
|
|
) -> None:
|
|
"""Test individual new Data API sensor values."""
|
|
device = create_tibber_device(sensor_values={sensor_id: expected_value})
|
|
data_api_client_mock.get_all_devices = AsyncMock(return_value={"device-id": device})
|
|
data_api_client_mock.update_devices = AsyncMock(return_value={"device-id": device})
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
unique_id = f"device-id_{sensor_id}"
|
|
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)
|
|
assert entity_id is not None, f"Entity not found for {description}"
|
|
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None, f"State not found for {description}"
|
|
assert float(state.state) == expected_value, (
|
|
f"Expected {expected_value} for {description}, got {state.state}"
|
|
)
|
|
|
|
|
|
async def test_new_data_api_sensors_with_disabled_by_default(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
data_api_client_mock: AsyncMock,
|
|
setup_credentials: None,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test that sensors with entity_registry_enabled_default=False are disabled."""
|
|
sensor_values = {
|
|
"cellular.rssi": -75.0,
|
|
"energyFlow.hour.battery.source.grid": 500.0,
|
|
"energyFlow.hour.battery.source.load": 300.0,
|
|
"energyFlow.hour.load.source.battery": 700.0,
|
|
"energyFlow.hour.load.source.grid": 500.0,
|
|
"energyFlow.month.battery.source.grid": 10000.0,
|
|
"energyFlow.month.battery.source.battery": 5000.0,
|
|
"energyFlow.month.battery.source.load": 8000.0,
|
|
"energyFlow.month.grid.source.battery": 3000.0,
|
|
"energyFlow.month.grid.source.grid": 1000.0,
|
|
"energyFlow.month.grid.source.load": 2000.0,
|
|
"energyFlow.month.load.source.battery": 15000.0,
|
|
"energyFlow.month.load.source.grid": 10000.0,
|
|
}
|
|
|
|
device = create_tibber_device(sensor_values=sensor_values)
|
|
data_api_client_mock.get_all_devices = AsyncMock(return_value={"device-id": device})
|
|
data_api_client_mock.update_devices = AsyncMock(return_value={"device-id": device})
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
disabled_sensors = [
|
|
"cellular.rssi",
|
|
"energyFlow.hour.battery.source.grid",
|
|
"energyFlow.hour.battery.source.load",
|
|
"energyFlow.hour.load.source.battery",
|
|
"energyFlow.hour.load.source.grid",
|
|
"energyFlow.month.battery.source.grid",
|
|
"energyFlow.month.battery.source.battery",
|
|
"energyFlow.month.battery.source.load",
|
|
"energyFlow.month.grid.source.battery",
|
|
"energyFlow.month.grid.source.grid",
|
|
"energyFlow.month.grid.source.load",
|
|
"energyFlow.month.load.source.battery",
|
|
"energyFlow.month.load.source.grid",
|
|
]
|
|
|
|
for sensor_id in disabled_sensors:
|
|
unique_id = f"device-id_{sensor_id}"
|
|
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)
|
|
assert entity_id is not None, f"Entity not found for sensor {sensor_id}"
|
|
|
|
entity_entry = entity_registry.async_get(entity_id)
|
|
assert entity_entry is not None
|
|
assert entity_entry.disabled, (
|
|
f"Sensor {sensor_id} should be disabled by default"
|
|
)
|