Files
core/tests/components/tibber/test_sensor.py
T

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"
)