diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 348ef91cd7d..31d3d77fd17 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -784,15 +784,22 @@ class MeterSensor(_FroniusSensorEntity): self._entity_id_prefix = f"meter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) meter_data = self._device_data() + # S0 meters connected directly to inverters respond "n.a." as serial number + # `model` contains the inverter id: "S0 Meter at inverter 1" + if (meter_uid := meter_data["serial"]["value"]) == "n.a.": + meter_uid = ( + f"{coordinator.solar_net.solar_net_device_id}:" + f'{meter_data["model"]["value"]}' + ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, meter_data["serial"]["value"])}, + identifiers={(DOMAIN, meter_uid)}, manufacturer=meter_data["manufacturer"]["value"], model=meter_data["model"]["value"], name=meter_data["model"]["value"], via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), ) - self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}' + self._attr_unique_id = f"{meter_uid}-{key}" class OhmpilotSensor(_FroniusSensorEntity): diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 683575a11c8..7930f6c01f4 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,4 +1,6 @@ """Tests for the Fronius integration.""" +from __future__ import annotations + from homeassistant.components.fronius.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -10,16 +12,16 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker MOCK_HOST = "http://fronius" -MOCK_UID = "123.4567890" # has to match mocked logger unique_id +MOCK_UID = "123.4567890" async def setup_fronius_integration( - hass: HomeAssistant, is_logger: bool = True + hass: HomeAssistant, is_logger: bool = True, unique_id: str = MOCK_UID ) -> ConfigEntry: """Create the Fronius integration.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=MOCK_UID, + unique_id=unique_id, # has to match mocked logger unique_id data={ CONF_HOST: MOCK_HOST, "is_logger": is_logger, @@ -35,9 +37,10 @@ def mock_responses( aioclient_mock: AiohttpClientMocker, host: str = MOCK_HOST, fixture_set: str = "symo", + inverter_ids: list[str | int] = [1], night: bool = False, ) -> None: - """Mock responses for Fronius Symo inverter with meter.""" + """Mock responses for Fronius devices.""" aioclient_mock.clear_requests() _night = "_night" if night else "" @@ -45,14 +48,15 @@ def mock_responses( f"{host}/solar_api/GetAPIVersion.cgi", text=load_fixture(f"{fixture_set}/GetAPIVersion.json", "fronius"), ) - aioclient_mock.get( - f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" - "DeviceId=1&DataCollection=CommonInverterData", - text=load_fixture( - f"{fixture_set}/GetInverterRealtimeData_Device_1{_night}.json", - "fronius", - ), - ) + for inverter_id in inverter_ids: + aioclient_mock.get( + f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" + f"DeviceId={inverter_id}&DataCollection=CommonInverterData", + text=load_fixture( + f"{fixture_set}/GetInverterRealtimeData_Device_{inverter_id}{_night}.json", + "fronius", + ), + ) aioclient_mock.get( f"{host}/solar_api/v1/GetInverterInfo.cgi", text=load_fixture(f"{fixture_set}/GetInverterInfo.json", "fronius"), diff --git a/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json new file mode 100644 index 00000000000..2051b4d58e3 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion" : 1, + "BaseURL" : "/solar_api/v1/", + "CompatibilityRange" : "1.6-3" +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json new file mode 100644 index 00000000000..5ac293653c0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json @@ -0,0 +1,33 @@ +{ + "Body" : { + "Data" : { + "1" : { + "CustomName" : "Primo 5.0-1", + "DT" : 76, + "ErrorCode" : 0, + "PVPower" : 5160, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "123456" + }, + "2" : { + "CustomName" : "Primo 3.0-1", + "DT" : 81, + "ErrorCode" : 0, + "PVPower" : 3240, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "234567" + } + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json new file mode 100644 index 00000000000..e54366a5008 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 22504 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60 + }, + "IAC" : { + "Unit" : "A", + "Value" : 3.8500000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 4.2300000000000004 + }, + "PAC" : { + "Unit" : "W", + "Value" : 862 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 17114940 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.90000000000001 + }, + "UDC" : { + "Unit" : "V", + "Value" : 452.30000000000001 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 7532755.5 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "1", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:08-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json new file mode 100644 index 00000000000..dd1e22c0a7a --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 14237 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60.009999999999998 + }, + "IAC" : { + "Unit" : "A", + "Value" : 1.3200000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 0.96999999999999997 + }, + "PAC" : { + "Unit" : "W", + "Value" : 296 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 5796010 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.59999999999999 + }, + "UDC" : { + "Unit" : "V", + "Value" : 329.5 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 3596193.25 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "2", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:36:15-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json new file mode 100644 index 00000000000..1fb0bbc8577 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json @@ -0,0 +1,29 @@ +{ + "Body" : { + "LoggerInfo" : { + "CO2Factor" : 0.52999997138977051, + "CO2Unit" : "kg", + "CashCurrency" : "BRL", + "CashFactor" : 1, + "DefaultLanguage" : "en", + "DeliveryFactor" : 1, + "HWVersion" : "2.4E", + "PlatformID" : "wilma", + "ProductID" : "fronius-datamanager-card", + "SWVersion" : "3.18.7-1", + "TimezoneLocation" : "Sao_Paulo", + "TimezoneName" : "-03", + "UTCOffset" : 4294956496, + "UniqueID" : "123.4567890" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:09-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json new file mode 100644 index 00000000000..aa308bb3b69 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json @@ -0,0 +1,31 @@ +{ + "Body" : { + "Data" : { + "0" : { + "Details" : { + "Manufacturer" : "Fronius", + "Model" : "S0 Meter at inverter 1", + "Serial" : "n.a." + }, + "Enable" : 1, + "EnergyReal_WAC_Minus_Relative" : 191.25, + "Meter_Location_Current" : 1, + "PowerReal_P_Sum" : -2216.7486858112229, + "TimeStamp" : 1639074843, + "Visible" : 1 + } + } + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "Meter", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:04-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000..4562b45efb0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json @@ -0,0 +1,17 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "OhmPilot", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json new file mode 100644 index 00000000000..4bbee2aec28 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json @@ -0,0 +1,45 @@ +{ + "Body" : { + "Data" : { + "Inverters" : { + "1" : { + "DT" : 76, + "E_Day" : 22502, + "E_Total" : 17114930, + "E_Year" : 7532753.5, + "P" : 886 + }, + "2" : { + "DT" : 81, + "E_Day" : 14222, + "E_Total" : 5795989.5, + "E_Year" : 3596179.75, + "P" : 948 + } + }, + "Site" : { + "E_Day" : 36724, + "E_Total" : 22910919.5, + "E_Year" : 11128933.25, + "Meter_Location" : "load", + "Mode" : "vague-meter", + "P_Akku" : null, + "P_Grid" : 384.93491437299008, + "P_Load" : -2218.9349143729901, + "P_PV" : 1834, + "rel_Autonomy" : 82.652266550064084, + "rel_SelfConsumption" : 100 + }, + "Version" : "12" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json new file mode 100644 index 00000000000..8743a2c6d68 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json @@ -0,0 +1,14 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 255, + "Reason" : "GetStorageRealtimeData request is not supported by this device.", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index cf371a47471..2e48faf606a 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Fronius sensor platform.""" +from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, FroniusMeterUpdateCoordinator, @@ -6,6 +7,7 @@ from homeassistant.components.fronius.coordinator import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers import device_registry as dr from homeassistant.util import dt from . import enable_all_entities, mock_responses, setup_fronius_integration @@ -371,7 +373,9 @@ async def test_gen24_storage(hass, aioclient_mock): assert state.state == str(expected_state) mock_responses(aioclient_mock, fixture_set="gen24_storage") - config_entry = await setup_fronius_integration(hass, is_logger=False) + config_entry = await setup_fronius_integration( + hass, is_logger=False, unique_id="12345678" + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 36 await enable_all_entities( @@ -469,3 +473,133 @@ async def test_gen24_storage(hass, aioclient_mock): assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_12345678")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.name == "SolarNet" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "12345678")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Gen24" + assert inverter_1.name == "Gen24 Storage" + + meter = device_registry.async_get_device(identifiers={(DOMAIN, "1234567890")}) + assert meter.manufacturer == "Fronius" + assert meter.model == "Smart Meter TS 65A-3" + assert meter.name == "Smart Meter TS 65A-3" + + ohmpilot = device_registry.async_get_device(identifiers={(DOMAIN, "23456789")}) + assert ohmpilot.manufacturer == "Fronius" + assert ohmpilot.model == "Ohmpilot 6" + assert ohmpilot.name == "Ohmpilot" + assert ohmpilot.sw_version == "1.0.25-3" + + storage = device_registry.async_get_device( + identifiers={(DOMAIN, "P030T020Z2001234567 ")} + ) + assert storage.manufacturer == "BYD" + assert storage.model == "BYD Battery-Box Premium HV" + assert storage.name == "BYD Battery-Box Premium HV" + + +async def test_primo_s0(hass, aioclient_mock): + """Test Fronius Primo dual inverter with S0 meter entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2]) + config_entry = await setup_fronius_integration(hass, is_logger=True) + + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30 + await enable_all_entities( + hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + ) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 41 + # logger + assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) + assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) + assert_state("sensor.delivery_factor_fronius_logger_info_0_http_fronius", 1) + # inverter 1 + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 17114940) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 22504) + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 452.3) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 862) + assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 4.23) + assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 7532755.5) + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 3.85) + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 223.9) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 60) + assert_state("sensor.led_color_fronius_inverter_1_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_1_http_fronius", 0) + # inverter 2 + assert_state("sensor.energy_total_fronius_inverter_2_http_fronius", 5796010) + assert_state("sensor.energy_day_fronius_inverter_2_http_fronius", 14237) + assert_state("sensor.voltage_dc_fronius_inverter_2_http_fronius", 329.5) + assert_state("sensor.power_ac_fronius_inverter_2_http_fronius", 296) + assert_state("sensor.error_code_fronius_inverter_2_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_2_http_fronius", 0.97) + assert_state("sensor.status_code_fronius_inverter_2_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_2_http_fronius", 3596193.25) + assert_state("sensor.current_ac_fronius_inverter_2_http_fronius", 1.32) + assert_state("sensor.voltage_ac_fronius_inverter_2_http_fronius", 223.6) + assert_state("sensor.frequency_ac_fronius_inverter_2_http_fronius", 60.01) + assert_state("sensor.led_color_fronius_inverter_2_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_2_http_fronius", 0) + # meter + assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 1) + assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) + # power_flow + assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) + assert_state( + "sensor.power_battery_fronius_power_flow_0_http_fronius", STATE_UNKNOWN + ) + assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") + assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) + assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100 + ) + assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 82.6523) + assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 22910919.5) + assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", 36724) + assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", 11128933.25) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.model == "fronius-datamanager-card" + assert solar_net.name == "SolarNet" + assert solar_net.sw_version == "3.18.7-1" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "123456")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Primo 5.0-1" + assert inverter_1.name == "Primo 5.0-1" + + inverter_2 = device_registry.async_get_device(identifiers={(DOMAIN, "234567")}) + assert inverter_2.manufacturer == "Fronius" + assert inverter_2.model == "Primo 3.0-1" + assert inverter_2.name == "Primo 3.0-1" + + meter = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890:S0 Meter at inverter 1")} + ) + assert meter.manufacturer == "Fronius" + assert meter.model == "S0 Meter at inverter 1" + assert meter.name == "S0 Meter at inverter 1"