diff --git a/homeassistant/components/victron_gx/button.py b/homeassistant/components/victron_gx/button.py index 032ffae69370b0..f94ae611dcaae4 100644 --- a/homeassistant/components/victron_gx/button.py +++ b/homeassistant/components/victron_gx/button.py @@ -51,6 +51,8 @@ def on_new_metric( class VictronButton(VictronBaseEntity, ButtonEntity): """Implementation of a Victron GX button entity.""" + _follow_metric_availability = False + @callback @override def _on_update_cb(self, _value: Any) -> None: diff --git a/homeassistant/components/victron_gx/device_tracker.py b/homeassistant/components/victron_gx/device_tracker.py index 58b8adda7b2a1e..956b0d0330e79c 100644 --- a/homeassistant/components/victron_gx/device_tracker.py +++ b/homeassistant/components/victron_gx/device_tracker.py @@ -1,12 +1,13 @@ """Support for Victron GX device tracker.""" -from typing import Any, override +from typing import TYPE_CHECKING, override from victron_mqtt import ( Device as VictronVenusDevice, GpsLocation, Metric as VictronVenusMetric, MetricKind, + MetricValue, ) from homeassistant.components.device_tracker import TrackerEntity @@ -63,11 +64,16 @@ def __init__( ) -> None: """Initialize the device tracker.""" super().__init__(device, metric, device_info, installation_id) - self._update_from_location(metric.value) + value = metric.value + if TYPE_CHECKING: + assert value is None or isinstance(value, GpsLocation) + self._update_from_location(value) @callback @override - def _on_update_cb(self, value: Any) -> None: + def _on_update_cb(self, value: MetricValue) -> None: + if TYPE_CHECKING: + assert value is None or isinstance(value, GpsLocation) self._update_from_location(value) self.async_write_ha_state() diff --git a/homeassistant/components/victron_gx/entity.py b/homeassistant/components/victron_gx/entity.py index 82ac0809d88b1d..7c7523101e3f60 100644 --- a/homeassistant/components/victron_gx/entity.py +++ b/homeassistant/components/victron_gx/entity.py @@ -1,12 +1,13 @@ """Base entity for entities in victron_gx integration.""" from abc import abstractmethod -from typing import Any, override +from typing import override from victron_mqtt import ( Device as VictronVenusDevice, Metric as VictronVenusMetric, MetricType, + MetricValue, ) from homeassistant.const import EntityCategory @@ -51,6 +52,7 @@ class VictronBaseEntity(Entity): _attr_should_poll = False _attr_has_entity_name = True + _follow_metric_availability = True def __init__( self, @@ -62,6 +64,8 @@ def __init__( """Initialize the entity.""" self._device = device self._metric = metric + if self._follow_metric_availability: + self._attr_available = metric.available self._attr_device_info = device_info self._attr_unique_id = f"{installation_id}_{metric.unique_id}" self._attr_suggested_display_precision = metric.precision @@ -111,11 +115,17 @@ def _resolve_native_unit_of_measurement(self) -> str | None: @callback @abstractmethod - def _on_update_cb(self, value: Any) -> None: + def _on_update_cb(self, value: MetricValue) -> None: """Handle the metric update. Must be implemented by subclasses.""" @callback - def _on_update(self, _: VictronVenusMetric, value: Any) -> None: + def _on_update(self, metric: VictronVenusMetric, value: MetricValue) -> None: + if self._follow_metric_availability and not metric.available: + if self._attr_available: + self._attr_available = False + self.async_write_ha_state() + return + self._attr_available = True self._on_update_cb(value) @override diff --git a/homeassistant/components/victron_gx/manifest.json b/homeassistant/components/victron_gx/manifest.json index d1832f469e87c8..82460492c8223b 100644 --- a/homeassistant/components/victron_gx/manifest.json +++ b/homeassistant/components/victron_gx/manifest.json @@ -7,7 +7,7 @@ "integration_type": "hub", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["victron-mqtt==2026.8.4"], + "requirements": ["victron-mqtt==2026.9.2"], "ssdp": [ { "X_MqttOnLan": "1", diff --git a/homeassistant/components/victron_gx/number.py b/homeassistant/components/victron_gx/number.py index 668507ec4310be..36922be55b16ae 100644 --- a/homeassistant/components/victron_gx/number.py +++ b/homeassistant/components/victron_gx/number.py @@ -1,12 +1,13 @@ """Support for Victron GX number entities.""" -from typing import TYPE_CHECKING, Any, override +from typing import TYPE_CHECKING, override from victron_mqtt import ( Device as VictronVenusDevice, Metric as VictronVenusMetric, MetricKind, MetricType, + MetricValue, WritableMetric as VictronVenusWritableMetric, ) @@ -77,7 +78,10 @@ def __init__( """Initialize the number entity.""" super().__init__(device, metric, device_info, installation_id) self._attr_device_class = METRIC_TYPE_TO_DEVICE_CLASS.get(metric.metric_type) - self._attr_native_value = metric.value + value = metric.value + if TYPE_CHECKING: + assert value is None or isinstance(value, int | float) + self._attr_native_value = value if metric.min_value is not None: self._attr_native_min_value = metric.min_value if metric.max_value is not None: @@ -93,7 +97,9 @@ def native_unit_of_measurement(self) -> str | None: @callback @override - def _on_update_cb(self, value: Any) -> None: + def _on_update_cb(self, value: MetricValue) -> None: + if TYPE_CHECKING: + assert value is None or isinstance(value, int | float) self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/victron_gx/strings.json b/homeassistant/components/victron_gx/strings.json index c81ec9e17a73c4..073c2227b2eeaa 100644 --- a/homeassistant/components/victron_gx/strings.json +++ b/homeassistant/components/victron_gx/strings.json @@ -338,6 +338,12 @@ "hub4_ac_grid_setpoint": { "name": "AC grid setpoint" }, + "hub4_max_charge_power": { + "name": "Maximum charge power" + }, + "hub4_max_discharge_power": { + "name": "Maximum discharge power" + }, "multi_ess_min_soc_limit": { "name": "ESS minimum SoC limit" }, @@ -347,6 +353,9 @@ "multiplus_assist_current_boost_factor": { "name": "Assist current boost factor" }, + "pvinverter_power_limit": { + "name": "Power limit" + }, "solarcharger_charge_current_limit": { "name": "[%key:component::victron_gx::common::charge_current_limit%]" }, @@ -987,6 +996,18 @@ "dcload_voltage": { "name": "[%key:component::victron_gx::common::voltage%]" }, + "dcsource_current": { + "name": "[%key:component::victron_gx::common::current%]" + }, + "dcsource_power": { + "name": "[%key:component::victron_gx::common::power%]" + }, + "dcsource_temperature": { + "name": "[%key:component::victron_gx::common::temperature%]" + }, + "dcsource_voltage": { + "name": "[%key:component::victron_gx::common::voltage%]" + }, "dcsystem_aux_voltage": { "name": "Auxiliary voltage" }, @@ -1525,6 +1546,21 @@ "platform_venus_firmware_installed_version": { "name": "Installed version" }, + "platform_venus_firmware_progress": { + "name": "Firmware update progress" + }, + "platform_venus_firmware_state": { + "name": "Firmware update state", + "state": { + "checking": "Checking", + "downloading_and_installing": "Downloading and installing", + "error_during_check": "Error during check", + "error_during_update": "Error during update", + "idle": "[%key:common::state::idle%]", + "rebooting": "Rebooting", + "update_file_not_found": "Update file not found" + } + }, "pvinverter_current_phase": { "name": "Current {phase}" }, diff --git a/homeassistant/components/victron_gx/time.py b/homeassistant/components/victron_gx/time.py index b6b426f8db9af4..633ad3f46e12df 100644 --- a/homeassistant/components/victron_gx/time.py +++ b/homeassistant/components/victron_gx/time.py @@ -2,12 +2,13 @@ from datetime import time import logging -from typing import TYPE_CHECKING, Any, override +from typing import TYPE_CHECKING, override from victron_mqtt import ( Device as VictronVenusDevice, Metric as VictronVenusMetric, MetricKind, + MetricValue, WritableMetric as VictronVenusWritableMetric, ) @@ -58,11 +59,16 @@ def __init__( ) -> None: """Initialize the time entity.""" super().__init__(device, metric, device_info, installation_id) - self._attr_native_value = VictronTime.victron_time_to_time(metric.value) + value = metric.value + if TYPE_CHECKING: + assert value is None or isinstance(value, int | float) + self._attr_native_value = VictronTime.victron_time_to_time(value) @callback @override - def _on_update_cb(self, value: Any) -> None: + def _on_update_cb(self, value: MetricValue) -> None: + if TYPE_CHECKING: + assert value is None or isinstance(value, int | float) self._attr_native_value = VictronTime.victron_time_to_time(value) self.async_write_ha_state() @@ -81,7 +87,7 @@ async def async_set_value(self, value: time) -> None: self._metric.set(total_minutes) @staticmethod - def victron_time_to_time(value: int | None) -> time | None: + def victron_time_to_time(value: float | None) -> time | None: """Convert minutes since midnight to time object.""" if value is None: return None diff --git a/requirements_all.txt b/requirements_all.txt index 74285d09ff1c6e..450192469b46d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3382,7 +3382,7 @@ viaggiatreno_ha==0.2.4 victron-ble-ha-parser==0.7.0 # homeassistant.components.victron_gx -victron-mqtt==2026.8.4 +victron-mqtt==2026.9.2 # homeassistant.components.victron_remote_monitoring victron-vrm==0.1.12 diff --git a/tests/components/victron_gx/test_device_tracker.py b/tests/components/victron_gx/test_device_tracker.py index e086cd2615a560..57694d729cfe49 100644 --- a/tests/components/victron_gx/test_device_tracker.py +++ b/tests/components/victron_gx/test_device_tracker.py @@ -1,10 +1,13 @@ """Tests for Victron GX MQTT device trackers.""" +from unittest.mock import MagicMock + from victron_mqtt import Hub as VictronVenusHub from victron_mqtt.testing import finalize_injection, inject_message from homeassistant.components.device_tracker import SourceType, TrackingType from homeassistant.components.victron_gx.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -122,6 +125,7 @@ async def test_victron_device_tracker( state = hass.states.get(entity.entity_id) assert state is not None + assert state.state == STATE_UNKNOWN assert state.attributes == { "source_type": SourceType.GPS, "altitude": None, @@ -131,3 +135,12 @@ async def test_victron_device_tracker( "in_zones": [], "tracking_type": TrackingType.POSITION, } + + metric = victron_hub.devices["gps_0"].get_metric("gps_location") + assert metric is not None + metric._keepalive(force_invalidate=True, log_debug=MagicMock()) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/victron_gx/test_number.py b/tests/components/victron_gx/test_number.py index 9c3395568f78db..08d36a39d2c76c 100644 --- a/tests/components/victron_gx/test_number.py +++ b/tests/components/victron_gx/test_number.py @@ -1,10 +1,13 @@ """Tests for Victron GX MQTT number entities.""" +from unittest.mock import MagicMock + from victron_mqtt import Hub as VictronVenusHub from victron_mqtt.testing import finalize_injection, inject_message from homeassistant.components.number import NumberDeviceClass from homeassistant.components.victron_gx.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -98,6 +101,54 @@ async def test_victron_number_update( assert state.state == "58.0" +async def test_nullable_number_availability( + hass: HomeAssistant, + init_integration: tuple[VictronVenusHub, MockConfigEntry], + entity_registry: er.EntityRegistry, +) -> None: + """Test nullable values remain distinct from source unavailability.""" + victron_hub, mock_config_entry = init_integration + topic = f"N/{MOCK_INSTALLATION_ID}/hub4/0/Overrides/MaxChargePower" + + await inject_message(victron_hub, topic, '{"value": 1200.0}') + await finalize_injection(victron_hub, disconnect=False) + await hass.async_block_till_done() + + entity = next( + entity + for entity in er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + if entity.translation_key == "hub4_max_charge_power" + ) + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == "1200.0" + + await inject_message(victron_hub, topic, '{"value": null}') + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == STATE_UNKNOWN + + metric = victron_hub.devices["hub4_0"].get_metric("hub4_max_charge_power") + assert metric is not None + metric._keepalive(force_invalidate=True, log_debug=MagicMock()) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await inject_message(victron_hub, topic, '{"value": 1300.0}') + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == "1300.0" + + async def test_victron_number_actions( hass: HomeAssistant, init_integration: tuple[VictronVenusHub, MockConfigEntry],