Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/victron_gx/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def on_new_metric(
class VictronButton(VictronBaseEntity, ButtonEntity):
"""Implementation of a Victron GX button entity."""

_follow_metric_availability = False
Comment thread
tomer-w marked this conversation as resolved.

@callback
@override
def _on_update_cb(self, _value: Any) -> None:
Expand Down
12 changes: 9 additions & 3 deletions homeassistant/components/victron_gx/device_tracker.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()

Expand Down
16 changes: 13 additions & 3 deletions homeassistant/components/victron_gx/entity.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -51,6 +52,7 @@ class VictronBaseEntity(Entity):

_attr_should_poll = False
_attr_has_entity_name = True
_follow_metric_availability = True

def __init__(
self,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/victron_gx/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Comment thread
tomer-w marked this conversation as resolved.
"ssdp": [
{
"X_MqttOnLan": "1",
Expand Down
12 changes: 9 additions & 3 deletions homeassistant/components/victron_gx/number.py
Original file line number Diff line number Diff line change
@@ -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,
)

Expand Down Expand Up @@ -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:
Expand All @@ -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()

Expand Down
36 changes: 36 additions & 0 deletions homeassistant/components/victron_gx/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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%]"
},
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -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}"
},
Expand Down
14 changes: 10 additions & 4 deletions homeassistant/components/victron_gx/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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()

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions tests/components/victron_gx/test_device_tracker.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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
51 changes: 51 additions & 0 deletions tests/components/victron_gx/test_number.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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],
Expand Down
Loading