Add sensors for detailed Enphase inverter readings (#146916)

* Add extra details to Enphase inverters

* Bump pyenphase version to 2.1.0

* Add new inverter sensors and translations

* Add new endpoint

* Start updating tests

* Remove duplicate class

* Add `max_reported` sensor

* Move translation strings to correct location

* Update fixtures and snapshots

* Update unit tests

* Fix linting

* Apply suggestions from code review

Co-authored-by: Arie Catsman <120491684+catsmanac@users.noreply.github.com>

* Fix Telegram bot parsing of inline keyboard (#146376)

* bug fix for inline keyboard

* update inline keyboard test

* Update tests/components/telegram_bot/test_telegram_bot.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* revert last_message_id and updated tests

* removed TypeError test

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Handle the new JSON payload from traccar clients (#147254)

* Set `entity_id`

* Update unit tests

* Bump aioamazondevices to 3.1.14 (#147257)

* Bump pyseventeentrack to 1.1.1 (#147253)

Update pyseventeentrack requirement to version 1.1.1

* Bump uiprotect to version 7.14.1 (#147280)

* Fix `state_class`es for energy production

* Make `max_reported` `name` more descriptive

* Update snapshots

* Reuse some translations

* Remove unnecessary translation keys

* Update unit tests

* Update homeassistant/components/enphase_envoy/strings.json

* Update homeassistant/components/enphase_envoy/strings.json

* Fix

---------

Co-authored-by: Arie Catsman <120491684+catsmanac@users.noreply.github.com>
Co-authored-by: hanwg <han.wuguang@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Joakim Sørensen <joasoe@proton.me>
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
Co-authored-by: Shai Ungar <shai.ungar@riskified.com>
Co-authored-by: Raphael Hehl <7577984+RaHehl@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Alex Biddulph
2025-06-24 04:59:18 +10:00
committed by GitHub
parent 6af290eb74
commit fc91047d8d
17 changed files with 6334 additions and 27 deletions

View File

@ -65,6 +65,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
"/ivp/ensemble/generator",
"/ivp/meters",
"/ivp/meters/readings",
"/ivp/pdm/device_data",
"/home",
]

View File

@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.0.1"],
"requirements": ["pyenphase==2.1.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@ -45,6 +45,7 @@ from homeassistant.const import (
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@ -80,6 +81,114 @@ INVERTER_SENSORS = (
device_class=SensorDeviceClass.POWER,
value_fn=attrgetter("last_report_watts"),
),
EnvoyInverterSensorEntityDescription(
key="dc_voltage",
translation_key="dc_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("dc_voltage"),
),
EnvoyInverterSensorEntityDescription(
key="dc_current",
translation_key="dc_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("dc_current"),
),
EnvoyInverterSensorEntityDescription(
key="ac_voltage",
translation_key="ac_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("ac_voltage"),
),
EnvoyInverterSensorEntityDescription(
key="ac_current",
translation_key="ac_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("ac_current"),
),
EnvoyInverterSensorEntityDescription(
key="ac_frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("ac_frequency"),
),
EnvoyInverterSensorEntityDescription(
key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=3,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=attrgetter("temperature"),
),
EnvoyInverterSensorEntityDescription(
key="lifetime_energy",
translation_key="lifetime_energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=attrgetter("lifetime_energy"),
),
EnvoyInverterSensorEntityDescription(
key="energy_today",
translation_key="energy_today",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
entity_registry_enabled_default=False,
value_fn=attrgetter("energy_today"),
),
EnvoyInverterSensorEntityDescription(
key="last_report_duration",
translation_key="last_report_duration",
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=attrgetter("last_report_duration"),
),
EnvoyInverterSensorEntityDescription(
key="energy_produced",
translation_key="energy_produced",
native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=3,
entity_registry_enabled_default=False,
value_fn=attrgetter("energy_produced"),
),
EnvoyInverterSensorEntityDescription(
key="max_reported",
translation_key="max_reported",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=attrgetter("max_report_watts"),
),
EnvoyInverterSensorEntityDescription(
key=LAST_REPORTED_KEY,
translation_key=LAST_REPORTED_KEY,

View File

@ -380,6 +380,33 @@
},
"aggregated_soc": {
"name": "Aggregated battery soc"
},
"dc_voltage": {
"name": "DC voltage"
},
"dc_current": {
"name": "DC current"
},
"ac_voltage": {
"name": "AC voltage"
},
"ac_current": {
"name": "AC current"
},
"lifetime_energy": {
"name": "Lifetime energy produced"
},
"energy_today": {
"name": "Energy produced today"
},
"energy_produced": {
"name": "Energy produced since previous report"
},
"max_reported": {
"name": "Lifetime maximum power"
},
"last_report_duration": {
"name": "Last report duration"
}
},
"switch": {

2
requirements_all.txt generated
View File

@ -1962,7 +1962,7 @@ pyeiscp==0.0.7
pyemoncms==0.1.1
# homeassistant.components.enphase_envoy
pyenphase==2.0.1
pyenphase==2.1.0
# homeassistant.components.envisalink
pyenvisalink==4.7

View File

@ -1634,7 +1634,7 @@ pyeiscp==0.0.7
pyemoncms==0.1.1
# homeassistant.components.enphase_envoy
pyenphase==2.0.1
pyenphase==2.1.0
# homeassistant.components.everlights
pyeverlights==0.1.0

View File

@ -38,9 +38,19 @@
"inverters": {
"1": {
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1
"last_report_date": 1750460765,
"last_report_watts": 116,
"max_report_watts": 325,
"dc_voltage": 33.793,
"dc_current": 3.668,
"ac_voltage": 243.438,
"ac_current": 0.504,
"ac_frequency": 50.01,
"temperature": 23,
"energy_produced": 32.254,
"energy_today": 134,
"lifetime_energy": 130209,
"last_report_duration": 903
}
},
"tariff": null,

View File

@ -78,7 +78,17 @@
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1
"max_report_watts": 1,
"dc_voltage": null,
"dc_current": null,
"ac_voltage": null,
"ac_current": null,
"ac_frequency": null,
"temperature": null,
"energy_produced": null,
"energy_today": null,
"lifetime_energy": null,
"last_report_duration": null
}
},
"tariff": {

View File

@ -220,7 +220,17 @@
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1
"max_report_watts": 1,
"dc_voltage": null,
"dc_current": null,
"ac_voltage": null,
"ac_current": null,
"ac_frequency": null,
"temperature": null,
"energy_produced": null,
"energy_today": null,
"lifetime_energy": null,
"last_report_duration": null
}
},
"tariff": {

View File

@ -208,7 +208,17 @@
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1
"max_report_watts": 1,
"dc_voltage": null,
"dc_current": null,
"ac_voltage": null,
"ac_current": null,
"ac_frequency": null,
"temperature": null,
"energy_produced": null,
"energy_today": null,
"lifetime_energy": null,
"last_report_duration": null
}
},
"tariff": {

View File

@ -412,7 +412,17 @@
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1
"max_report_watts": 1,
"dc_voltage": null,
"dc_current": null,
"ac_voltage": null,
"ac_current": null,
"ac_frequency": null,
"temperature": null,
"energy_produced": null,
"energy_today": null,
"lifetime_energy": null,
"last_report_duration": null
}
},
"tariff": {

View File

@ -227,7 +227,17 @@
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1
"max_report_watts": 1,
"dc_voltage": null,
"dc_current": null,
"ac_voltage": null,
"ac_current": null,
"ac_frequency": null,
"temperature": null,
"energy_produced": null,
"energy_today": null,
"lifetime_energy": null,
"last_report_duration": null
}
},
"tariff": {

View File

@ -73,7 +73,17 @@
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1
"max_report_watts": 1,
"dc_voltage": null,
"dc_current": null,
"ac_voltage": null,
"ac_current": null,
"ac_frequency": null,
"temperature": null,
"energy_produced": null,
"energy_today": null,
"lifetime_energy": null,
"last_report_duration": null
}
},
"tariff": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,7 @@ async def test_with_pre_v7_firmware(
await setup_integration(hass, config_entry)
assert (entity_state := hass.states.get("sensor.inverter_1"))
assert entity_state.state == "1"
assert entity_state.state == "116"
@pytest.mark.freeze_time("2024-07-23 00:00:00+00:00")
@ -85,7 +85,7 @@ async def test_token_in_config_file(
await setup_integration(hass, entry)
assert (entity_state := hass.states.get("sensor.inverter_1"))
assert entity_state.state == "1"
assert entity_state.state == "116"
@respx.mock
@ -128,7 +128,7 @@ async def test_expired_token_in_config(
await setup_integration(hass, entry)
assert (entity_state := hass.states.get("sensor.inverter_1"))
assert entity_state.state == "1"
assert entity_state.state == "116"
async def test_coordinator_update_error(
@ -226,7 +226,7 @@ async def test_coordinator_token_refresh_error(
await setup_integration(hass, entry)
assert (entity_state := hass.states.get("sensor.inverter_1"))
assert entity_state.state == "1"
assert entity_state.state == "116"
async def test_config_no_unique_id(

View File

@ -772,6 +772,70 @@ async def test_sensor_inverter_data(
) == dt_util.utc_from_timestamp(inverter.last_report_date)
@pytest.mark.parametrize(
("mock_envoy"),
[
"envoy",
],
indirect=["mock_envoy"],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_inverter_detailed_data(
hass: HomeAssistant,
mock_envoy: AsyncMock,
config_entry: MockConfigEntry,
) -> None:
"""Test enphase_envoy inverter detailed entities values."""
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, config_entry)
entity_base = f"{Platform.SENSOR}.inverter"
for sn, inverter in mock_envoy.data.inverters.items():
assert (dc_voltage := hass.states.get(f"{entity_base}_{sn}_dc_voltage"))
assert float(dc_voltage.state) == (inverter.dc_voltage)
assert (dc_current := hass.states.get(f"{entity_base}_{sn}_dc_current"))
assert float(dc_current.state) == (inverter.dc_current)
assert (ac_voltage := hass.states.get(f"{entity_base}_{sn}_ac_voltage"))
assert float(ac_voltage.state) == (inverter.ac_voltage)
assert (ac_current := hass.states.get(f"{entity_base}_{sn}_ac_current"))
assert float(ac_current.state) == (inverter.ac_current)
assert (frequency := hass.states.get(f"{entity_base}_{sn}_frequency"))
assert float(frequency.state) == (inverter.ac_frequency)
assert (temperature := hass.states.get(f"{entity_base}_{sn}_temperature"))
assert int(temperature.state) == (inverter.temperature)
assert (
lifetime_energy := hass.states.get(
f"{entity_base}_{sn}_lifetime_energy_produced"
)
)
assert float(lifetime_energy.state) == (inverter.lifetime_energy / 1000.0)
assert (
energy_produced_today := hass.states.get(
f"{entity_base}_{sn}_energy_produced_today"
)
)
assert int(energy_produced_today.state) == (inverter.energy_today)
assert (
last_report_duration := hass.states.get(
f"{entity_base}_{sn}_last_report_duration"
)
)
assert int(last_report_duration.state) == (inverter.last_report_duration)
assert (
energy_produced := hass.states.get(
f"{entity_base}_{sn}_energy_produced_since_previous_report"
)
)
assert float(energy_produced.state) == (inverter.energy_produced)
assert (
lifetime_maximum_power := hass.states.get(
f"{entity_base}_{sn}_lifetime_maximum_power"
)
)
assert int(lifetime_maximum_power.state) == (inverter.max_report_watts)
@pytest.mark.parametrize(
("mock_envoy"),
[
@ -797,9 +861,23 @@ async def test_sensor_inverter_disabled_by_integration(
INVERTER_BASE = f"{Platform.SENSOR}.inverter"
assert all(
f"{INVERTER_BASE}_{sn}_last_reported"
f"{INVERTER_BASE}_{sn}_{key}"
in integration_disabled_entities(entity_registry, config_entry)
for sn in mock_envoy.data.inverters
for key in (
"dc_voltage",
"dc_current",
"ac_voltage",
"ac_current",
"frequency",
"temperature",
"lifetime_energy_produced",
"energy_produced_today",
"last_report_duration",
"energy_produced_since_previous_report",
"last_reported",
"lifetime_maximum_power",
)
)