"""The sensor tests for the powerwall platform.""" from datetime import timedelta from unittest.mock import Mock, patch import pytest from tesla_powerwall import MetersAggregatesResponse from tesla_powerwall.error import MissingAttributeError from homeassistant.components.powerwall.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, PERCENTAGE, STATE_UNKNOWN, UnitOfPower, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util from .mocks import ( MOCK_GATEWAY_DIN, _mock_powerwall_restricted, _mock_powerwall_with_fixtures, ) from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors(hass: HomeAssistant, device_registry: dr.DeviceRegistry) -> None: """Test creation of the sensors.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass) config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) with ( patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() reg_device = device_registry.async_get_device( identifiers={("powerwall", MOCK_GATEWAY_DIN)}, ) assert reg_device.model == "PowerWall 2 (GW1)" assert reg_device.sw_version == "1.50.1 c58c2df3" assert reg_device.manufacturer == "Tesla" assert reg_device.name == "MySite" battery_device = device_registry.async_get_device_by_identifier( (DOMAIN, f"{MOCK_GATEWAY_DIN}_TG0123456789AB"), config_entry.entry_id ) assert battery_device is not None assert battery_device.via_device_id == reg_device.id state = hass.states.get("sensor.mysite_load_power") assert state.state == "1.971" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "power" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "kW" assert attributes[ATTR_STATE_CLASS] == "measurement" assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load power" state = hass.states.get("sensor.mysite_load_frequency") assert state.state == "60" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "frequency" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "Hz" assert attributes[ATTR_STATE_CLASS] == "measurement" assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load frequency" state = hass.states.get("sensor.mysite_load_voltage") assert state.state == "120.7" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "voltage" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "V" assert attributes[ATTR_STATE_CLASS] == "measurement" assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load voltage" state = hass.states.get("sensor.mysite_load_current") assert state.state == "0" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "current" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "A" assert attributes[ATTR_STATE_CLASS] == "measurement" assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load current" assert float(hass.states.get("sensor.mysite_load_export").state) == 1056.8 assert float(hass.states.get("sensor.mysite_load_import").state) == 4693.0 state = hass.states.get("sensor.mysite_battery_power") assert state.state == "-8.55" assert float(hass.states.get("sensor.mysite_battery_export").state) == 3620.0 assert float(hass.states.get("sensor.mysite_battery_import").state) == 4216.2 state = hass.states.get("sensor.mysite_solar_power") assert state.state == "10.49" assert float(hass.states.get("sensor.mysite_solar_export").state) == 9864.2 assert float(hass.states.get("sensor.mysite_solar_import").state) == 28.2 state = hass.states.get("sensor.mysite_charge") assert state.state == "47" expected_attributes = { "unit_of_measurement": PERCENTAGE, "friendly_name": "MySite Charge", "device_class": "battery", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears for key, value in expected_attributes.items(): assert state.attributes[key] == value state = hass.states.get("sensor.mysite_backup_reserve") assert state.state == "15" expected_attributes = { "unit_of_measurement": PERCENTAGE, "friendly_name": "MySite Backup reserve", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears for key, value in expected_attributes.items(): assert state.attributes[key] == value state = hass.states.get("sensor.mysite_max_charge_power") assert state.state == "7000" expected_attributes = { "unit_of_measurement": UnitOfPower.WATT, "device_class": "power", "state_class": "measurement", "friendly_name": "MySite Max charge power", } for key, value in expected_attributes.items(): assert state.attributes[key] == value state = hass.states.get("sensor.mysite_max_discharge_power") assert state.state == "8380" expected_attributes = { "unit_of_measurement": UnitOfPower.WATT, "device_class": "power", "state_class": "measurement", "friendly_name": "MySite Max discharge power", } for key, value in expected_attributes.items(): assert state.attributes[key] == value state = hass.states.get("sensor.mysite_operation_mode") assert state.state == "self_consumption" assert state.attributes[ATTR_DEVICE_CLASS] == "enum" assert state.attributes[ATTR_FRIENDLY_NAME] == "MySite Operation mode" mock_powerwall.get_meters.return_value = MetersAggregatesResponse.from_dict({}) mock_powerwall.get_backup_reserve_percentage.return_value = None mock_powerwall.get_operation_mode.return_value = None async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) await hass.async_block_till_done() assert hass.states.get("sensor.mysite_load_power").state == STATE_UNKNOWN assert hass.states.get("sensor.mysite_load_frequency").state == STATE_UNKNOWN assert hass.states.get("sensor.mysite_backup_reserve").state == STATE_UNKNOWN assert hass.states.get("sensor.mysite_operation_mode").state == STATE_UNKNOWN assert ( float(hass.states.get("sensor.mysite_tg0123456789ab_battery_capacity").state) == 14.715 ) assert ( float(hass.states.get("sensor.mysite_tg0123456789ab_battery_voltage").state) == 245.7 ) assert ( float(hass.states.get("sensor.mysite_tg0123456789ab_frequency").state) == 50.0 ) assert float(hass.states.get("sensor.mysite_tg0123456789ab_current").state) == 0.3 assert int(hass.states.get("sensor.mysite_tg0123456789ab_power").state) == -100 assert ( float(hass.states.get("sensor.mysite_tg0123456789ab_battery_export").state) == 2358.235 ) assert ( float(hass.states.get("sensor.mysite_tg0123456789ab_battery_import").state) == 2693.355 ) assert ( float(hass.states.get("sensor.mysite_tg0123456789ab_battery_remaining").state) == 14.715 ) assert ( str(hass.states.get("sensor.mysite_tg0123456789ab_grid_state").state) == "grid_compliant" ) assert ( float(hass.states.get("sensor.mysite_tg9876543210ba_battery_capacity").state) == 15.137 ) assert ( float(hass.states.get("sensor.mysite_tg9876543210ba_battery_voltage").state) == 245.6 ) assert ( float(hass.states.get("sensor.mysite_tg9876543210ba_frequency").state) == 50.0 ) assert float(hass.states.get("sensor.mysite_tg9876543210ba_current").state) == 0.1 assert int(hass.states.get("sensor.mysite_tg9876543210ba_power").state) == -100 assert ( float(hass.states.get("sensor.mysite_tg9876543210ba_battery_export").state) == 509.907 ) assert ( float(hass.states.get("sensor.mysite_tg9876543210ba_battery_import").state) == 610.483 ) assert ( float(hass.states.get("sensor.mysite_tg9876543210ba_battery_remaining").state) == 15.137 ) assert ( str(hass.states.get("sensor.mysite_tg9876543210ba_grid_state").state) == "grid_compliant" ) async def test_sensor_backup_reserve_unavailable(hass: HomeAssistant) -> None: """Confirm backup reserve sensor is not added if unavailable.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass) mock_powerwall.get_backup_reserve_percentage.side_effect = MissingAttributeError( Mock(), "backup_reserve_percent", "operation" ) config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) with ( patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.powerwall_backup_reserve") assert state is None @pytest.mark.parametrize( ("method_name", "attribute", "entity_id"), [ ( "get_instantaneous_max_charge_power", "instantaneous_max_charge_power", "sensor.mysite_max_charge_power", ), ( "get_instantaneous_max_discharge_power", "instantaneous_max_discharge_power", "sensor.mysite_max_discharge_power", ), ], ) async def test_sensor_max_power_unavailable( hass: HomeAssistant, method_name: str, attribute: str, entity_id: str ) -> None: """Confirm max power sensors are not added if unavailable.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass) getattr(mock_powerwall, method_name).side_effect = MissingAttributeError( Mock(), attribute, "system_status" ) config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) with ( patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(entity_id) is None async def test_sensor_operation_mode_unavailable(hass: HomeAssistant) -> None: """Confirm operation mode sensor is not added if unavailable.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass) mock_powerwall.get_operation_mode.side_effect = MissingAttributeError( Mock(), "real_mode", "operation" ) config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) with ( patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.mysite_operation_mode") assert state is None async def test_sensors_with_empty_meters(hass: HomeAssistant) -> None: """Test creation of the sensors with empty meters.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass, empty_meters=True) config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) with ( patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get("sensor.mysite_solar_power") is None @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_unique_id_migrate( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test we can migrate unique ids of the sensors.""" config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) mock_powerwall = await _mock_powerwall_with_fixtures(hass) old_unique_id = "_".join(sorted(["TG0123456789AB", "TG9876543210BA"])) new_unique_id = MOCK_GATEWAY_DIN device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={("powerwall", old_unique_id)}, manufacturer="Tesla", ) old_mysite_load_power_entity = entity_registry.async_get_or_create( "sensor", DOMAIN, unique_id=f"{old_unique_id}_load_instant_power", suggested_object_id="mysite_load_power", config_entry=config_entry, ) assert old_mysite_load_power_entity.entity_id == "sensor.mysite_load_power" with ( patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() reg_device = device_registry.async_get_device( identifiers={("powerwall", MOCK_GATEWAY_DIN)}, ) old_reg_device = device_registry.async_get_device( identifiers={("powerwall", old_unique_id)}, ) assert old_reg_device is None assert reg_device is not None assert ( entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{old_unique_id}_load_instant_power" ) is None ) assert ( entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{new_unique_id}_load_instant_power" ) is not None ) state = hass.states.get("sensor.mysite_load_power") assert state.state == "1.971" async def test_pw3_restricted_entities( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Restricted PW3 surface should only expose meter-driven sensors and charge.""" mock_powerwall = await _mock_powerwall_restricted(hass) config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}, unique_id="aa:bb:cc:dd:ee:ff", ) config_entry.add_to_hass(hass) with ( patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() reg_device = device_registry.async_get_device( identifiers={("powerwall", "aa:bb:cc:dd:ee:ff")}, ) assert reg_device.model == "PowerWall 2" assert reg_device.sw_version is None assert reg_device.manufacturer == "Tesla" assert reg_device.name == "Powerwall 1.2.3.4" assert hass.states.get("sensor.powerwall_1_2_3_4_charge") is not None assert hass.states.get("sensor.powerwall_1_2_3_4_load_power") is not None assert hass.states.get("sensor.powerwall_1_2_3_4_battery_power") is not None # Backup reserve and per-battery sensors must not be created. assert hass.states.get("sensor.powerwall_1_2_3_4_backup_reserve") is None assert ( hass.states.get("sensor.powerwall_1_2_3_4_tg0123456789ab_battery_capacity") is None )