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: 1 addition & 1 deletion custom_components/victron_mqtt/_vendor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Vendored third-party packages."""

VICTRON_MQTT_VERSION = "2026.6.7"
VICTRON_MQTT_VERSION = "2026.6.8"
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
TemperatureStatus,
TemperatureType,
VictronDeviceEnum,
VrmPortalMode,
)
from .constants import MetricKind, MetricNature, MetricType, OperationMode, RangeType, VictronEnum
from .data_classes import GpsLocation
Expand Down Expand Up @@ -120,5 +121,6 @@
"TopicNotFoundError",
"VictronDeviceEnum",
"VictronEnum",
"VrmPortalMode",
"WritableMetric",
]
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,24 @@ def wrap_bitmask(
return json.dumps({"value": val})


def wrap_int(value: int | None) -> str:
def wrap_int(value: int | float | None) -> str:
"""Wrap an integer value into a JSON string with a 'value' key."""
return json.dumps({"value": value})
return json.dumps({"value": int(value) if value is not None else None})


def wrap_int_hours_to_seconds(value: int | None) -> str:
def wrap_int_hours_to_seconds(value: int | float | None) -> str:
"""Wrap an integer value into a JSON string with a 'value' key."""
return json.dumps({"value": value * 3600 if value is not None else None})
return json.dumps({"value": int(value) * 3600 if value is not None else None})


def wrap_int_minutes_to_seconds(value: int | None) -> str:
def wrap_int_minutes_to_seconds(value: int | float | None) -> str:
"""Wrap an integer value into a JSON string with a 'value' key."""
return json.dumps({"value": value * 60 if value is not None else None})
return json.dumps({"value": int(value) * 60 if value is not None else None})


def wrap_int_default_0(value: int | None) -> str:
def wrap_int_default_0(value: int | float | None) -> str:
"""Wrap an integer value into a JSON string with a 'value' key, defaulting to 0 if None."""
return json.dumps({"value": value if value is not None else 0})
return json.dumps({"value": int(value) if value is not None else 0})


def wrap_float(value: float | None) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DeviceType(VictronDeviceEnum):
) # Orion XS 1400 in alternator to battery charging mode.
SWITCH = ("switch", "switch", "Switch")
GPS = ("gps", "gps", "GPS")
SYSTEM_SETUP = ("SystemSetup", "system_setup", "System setup", "system") # Should be mapped to SYSTEM
SYSTEM_SETUP = ("SystemSetup", "system_setup", "<Not used>", "system") # Should be mapped to SYSTEM
SERVICES = ("Services", "services", "<Not used>", "system") # Should be mapped to SYSTEM
TRANSFER_SWITCH = ("TransferSwitch", "transfer_switch", "Transfer switch")
DIGITAL_INPUT = ("digitalinput", "digital_input", "Digital input")
Expand All @@ -46,12 +46,13 @@ class DeviceType(VictronDeviceEnum):
"system",
) # For whatever reason some system topics are under platform
HEATPUMP = ("heatpump", "heatpump", "Heat pump")
NETWORK = ("Network", "network", "<Not used>", "system") # Network settings are under system
METEO = ("meteo", "meteo", "Irradiance sensor")
DYNAMIC_ESS = ("DynamicEss", "dynamic_ess", "Dynamic ESS", "system") # Dynamic ESS settings are under system
DYNAMIC_ESS = ("DynamicEss", "dynamic_ess", "<Not used>", "system") # Dynamic ESS settings are under system
ACLOAD = ("acload", "acload", "AC load")
CHARGER = ("charger", "charger", "Charger")
HUB4 = ("hub4", "hub4", "Hub4")
ACSYSTEM = ("acsystem", "acsystem", "AC system", "system") # Should be mapped to SYSTEM
ACSYSTEM = ("acsystem", "acsystem", "<Not used>", "system") # Should be mapped to SYSTEM
DCDC = ("dcdc", "dcdc", "DC/DC charger") # Orion XS 1400 in battery to battery charging mode.


Expand Down Expand Up @@ -83,6 +84,14 @@ class GenericOnOffInverted(VictronEnum):
OFF = (1, "off", "Off")


class VrmPortalMode(VictronEnum):
"""VRM Portal access level enum."""

OFF = (0, "off", "Off")
READ_ONLY = (1, "read_only", "Read-only")
FULL = (2, "full", "Full")


class PreferRenewableEnergyEnum(VictronEnum):
"""Prefer Renewable Energy state.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Maps all the MQTT topics to either attributes or metrics.
Defines the TopicDescriptor mappings for Victron MQTT topics and derived formula topics.
"""

from ._victron_enums import (
Expand Down Expand Up @@ -42,6 +42,7 @@
SwitchableOutputType,
TemperatureStatus,
TemperatureType,
VrmPortalMode,
)
from .constants import MetricKind, MetricNature, MetricType, RangeType, ValueType
from .data_classes import TopicDependency, TopicDescriptor
Expand Down Expand Up @@ -2001,12 +2002,10 @@
),
TopicDescriptor(
topic="N/{installation_id}/multi/{device_id}/Ess/AcPowerSetpoint",
message_type=MetricKind.NUMBER,
message_type=MetricKind.SENSOR,
short_id="multi_ess_ac_power_setpoint",
name="ESS AC power setpoint",
metric_type=MetricType.POWER,
min=-12500,
max=12500,
),
TopicDescriptor(
topic="N/{installation_id}/multi/{device_id}/Ess/DisableCharge",
Expand Down Expand Up @@ -2253,6 +2252,14 @@
max=10000, # Dynamic range, depends on device model
step=10,
),
TopicDescriptor(
topic="N/{installation_id}/settings/{device_id}/Settings/CGwacs/AlwaysPeakShave",
message_type=MetricKind.SWITCH,
short_id="system_ess_always_peak_shave",
name="ESS always peak shave",
value_type=ValueType.ENUM,
enum=GenericOnOff,
),
TopicDescriptor(
topic="N/{installation_id}/settings/{device_id}/Settings/CGwacs/BatteryLife/MinimumSocLimit",
message_type=MetricKind.NUMBER,
Expand Down Expand Up @@ -2634,6 +2641,15 @@
max=1800,
depends_on=["generator_{gen_id}_generator_autorun"],
),
# Network settings
TopicDescriptor(
topic="N/{installation_id}/settings/{device_id}/Settings/Network/VrmPortal",
message_type=MetricKind.SELECT,
short_id="system_vrm_portal_mode",
name="VRM portal access level",
value_type=ValueType.ENUM,
enum=VrmPortalMode,
),
# Relay Custom Name topics
TopicDescriptor(
topic="N/{installation_id}/settings/{device_id}/Settings/Relay/{relay}/CustomName",
Expand Down Expand Up @@ -3878,6 +3894,30 @@
name="Energy from out to inverter",
metric_type=MetricType.ENERGY,
),
TopicDescriptor(
topic="N/{installation_id}/vebus/{device_id}/Hub4/DoNotFeedInOvervoltage",
message_type=MetricKind.SWITCH,
short_id="vebus_hub4_do_not_feed_in_overvoltage",
name="Hub4 do not feed in on overvoltage",
value_type=ValueType.ENUM,
enum=GenericOnOff,
),
TopicDescriptor(
topic="N/{installation_id}/vebus/{device_id}/Hub4/FixSolarOffsetTo100mV",
message_type=MetricKind.SWITCH,
short_id="vebus_hub4_fix_solar_offset_100mv",
name="Hub4 fix solar offset to 100mV",
value_type=ValueType.ENUM,
enum=GenericOnOff,
),
TopicDescriptor(
topic="N/{installation_id}/vebus/{device_id}/Hub4/TargetPowerIsMaxFeedIn",
message_type=MetricKind.SWITCH,
short_id="vebus_hub4_target_power_is_max_feed_in",
name="Hub4 target power is max feed-in",
value_type=ValueType.ENUM,
enum=GenericOnOff,
),
TopicDescriptor(
topic="N/{installation_id}/vebus/{device_id}/Hub4/{phase}/AcPowerSetpoint",
message_type=MetricKind.NUMBER,
Expand All @@ -3896,6 +3936,14 @@
enum=InverterMode,
main_topic=True,
),
TopicDescriptor(
topic="N/{installation_id}/vebus/{device_id}/PvInverter/Disable",
message_type=MetricKind.SWITCH,
short_id="vebus_pvinverter_disable",
name="Vebus PV inverter disable",
value_type=ValueType.ENUM,
enum=GenericOnOff,
),
TopicDescriptor(
topic="N/{installation_id}/vebus/{device_id}/Settings/Alarm/System/GridLost",
message_type=MetricKind.SWITCH,
Expand Down Expand Up @@ -3933,4 +3981,13 @@
unit_of_measurement="resets",
value_type=ValueType.INT,
),
TopicDescriptor(
topic="W/{installation_id}/multi/{device_id}/Ess/AcPowerSetpoint",
message_type=MetricKind.SERVICE,
short_id="multi_service_ess_ac_power_setpoint",
name="Set ESS AC power setpoint",
metric_type=MetricType.POWER,
min=-12500,
max=12500,
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,27 @@ def unique_id(self) -> str:
assert self._unique_id is not None, f"unique_id is None for topic: {self.full_topic}"
return self._unique_id

@property
def display_id(self) -> str:
"""Get a display identifier with the device-type prefix de-duplicated.

The ``unique_id`` has a known formatting quirk: when ``short_id`` already
starts with the device-type prefix (e.g. ``solarcharger_total_pv_yield``
under device ``solarcharger_3``), the resulting id contains the prefix
twice — ``solarcharger_3_solarcharger_total_pv_yield``. ``display_id``
strips the redundant leading prefix, yielding ``solarcharger_3_total_pv_yield``.

``unique_id`` is intentionally left unchanged for backward compatibility
with existing consumers. ``display_id`` is provided for new consumers
that can adopt the cleaner form going forward.
"""
assert self._short_id is not None, f"short_id is None for topic: {self.full_topic}"
device_prefix = f"{self.device_type.code}_"
short = self._short_id
if short.startswith(device_prefix):
short = short[len(device_prefix):]
return ParsedTopic.make_unique_id(self.get_device_unique_id(), short)

@property
def key_values(self) -> dict[str, str]:
"""Get the key values of the ParsedTopic."""
Expand Down
2 changes: 2 additions & 0 deletions custom_components/victron_mqtt/_vendor/victron_mqtt/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ def _create_metric_from_placeholder(
name=name,
descriptor=new_topic_desc,
unique_id=metric_placeholder.parsed_topic.unique_id,
display_id=metric_placeholder.parsed_topic.display_id,
short_id=metric_placeholder.parsed_topic.short_id,
key_values=metric_placeholder.parsed_topic.key_values,
topic=metric_placeholder.parsed_topic.full_topic,
Expand All @@ -269,6 +270,7 @@ def _create_metric_from_placeholder(
name=name,
descriptor=new_topic_desc,
unique_id=metric_placeholder.parsed_topic.unique_id,
display_id=metric_placeholder.parsed_topic.display_id,
short_id=metric_placeholder.parsed_topic.short_id,
key_values=metric_placeholder.parsed_topic.key_values,
hub=hub,
Expand Down
13 changes: 13 additions & 0 deletions custom_components/victron_mqtt/_vendor/victron_mqtt/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
name: str | None = None,
descriptor: TopicDescriptor | None = None,
unique_id: str | None = None,
display_id: str | None = None,
short_id: str | None = None,
key_values: dict[str, str] | None = None,
hub: Hub | None = None,
Expand All @@ -58,6 +59,7 @@ def __init__(
self._device: Device = device
self._descriptor: TopicDescriptor = descriptor
self._unique_id: str = unique_id
self._display_id: str = display_id if display_id is not None else unique_id
self._value: Any = None
self._short_id: str = short_id
self._name: str = name
Expand Down Expand Up @@ -197,6 +199,17 @@ def unique_id(self) -> str:
"""Return the unique id of the metric."""
return self._unique_id

@property
def display_id(self) -> str:
"""Return the display identifier with the device-type prefix de-duplicated.

Equal to ``unique_id`` for most metrics. Where ``short_id`` starts with
the device-type prefix, the redundant copy is stripped, giving a cleaner
identifier. See :attr:`ParsedTopic.display_id` for details.
The ``unique_id`` is never modified.
"""
return self._display_id

@property
def key_values(self) -> dict[str, str]:
"""Return the key_values dictionary as read-only."""
Expand Down
Loading
Loading