[ci skip] home assistant integration
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,4 +23,8 @@ _testmain.go
|
|||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
|
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
# Python cache files (for Home Assistant custom component)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
114
README.md
114
README.md
@@ -13,6 +13,7 @@ The code has been updated to support more of the protocol published by Victron a
|
|||||||
This project is based on the original open source `invertergui` project by Hendrik van Wyk and contributors:
|
This project is based on the original open source `invertergui` project by Hendrik van Wyk and contributors:
|
||||||
|
|
||||||
- Original repository: https://github.com/diebietse/invertergui
|
- Original repository: https://github.com/diebietse/invertergui
|
||||||
|
- Home Assistant `victron-mk3-hass` inspiration: https://github.com/j9brown/victron-mk3-hass
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
@@ -428,6 +429,119 @@ plus remote panel controls for:
|
|||||||
The combined mode + current limit behavior is provided through the `panel_state` MQTT command kind,
|
The combined mode + current limit behavior is provided through the `panel_state` MQTT command kind,
|
||||||
which mirrors `victron_mk3.set_remote_panel_state`.
|
which mirrors `victron_mk3.set_remote_panel_state`.
|
||||||
|
|
||||||
|
### Home Assistant Custom Component (MQTT)
|
||||||
|
|
||||||
|
This repository also includes a custom Home Assistant integration at:
|
||||||
|
|
||||||
|
- `custom_components/victron_mk2_mqtt`
|
||||||
|
|
||||||
|
This component is useful if you want HA entities/services that are explicitly tied to
|
||||||
|
`invertergui` MQTT topics, instead of relying only on MQTT auto-discovery entities.
|
||||||
|
|
||||||
|
If you use this custom component, you can disable `--mqtt.ha.enabled` in `invertergui`
|
||||||
|
to avoid duplicate entities created by MQTT discovery.
|
||||||
|
|
||||||
|
Install via HACS:
|
||||||
|
|
||||||
|
1. Add the integration repository in Home Assistant:
|
||||||
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=nathan&repository=invertergui&category=integration)
|
||||||
|
2. Install `Victron MK2 MQTT` from HACS.
|
||||||
|
3. Restart Home Assistant.
|
||||||
|
4. Add the YAML configuration shown below.
|
||||||
|
|
||||||
|
If you are not mirroring this repo to GitHub, use the manual install method below.
|
||||||
|
|
||||||
|
Manual install (alternative):
|
||||||
|
|
||||||
|
```text
|
||||||
|
<home-assistant-config>/custom_components/victron_mk2_mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add YAML config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
victron_mk2_mqtt:
|
||||||
|
name: Victron Inverter
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
command_topic: invertergui/settings/set
|
||||||
|
status_topic: invertergui/settings/status
|
||||||
|
# topic_root is optional; defaults to state_topic root (for example "invertergui")
|
||||||
|
# topic_root: invertergui
|
||||||
|
```
|
||||||
|
|
||||||
|
Provided entities include:
|
||||||
|
|
||||||
|
- Telemetry sensors (battery/input/output voltage/current/frequency and derived power)
|
||||||
|
- `Remote Panel Mode` (`charger_only`, `inverter_only`, `on`, `off`)
|
||||||
|
- `Remote Panel Current Limit` (A)
|
||||||
|
- `Remote Panel Standby`
|
||||||
|
- Diagnostic entities (`Data Valid`, `Last Command Error`)
|
||||||
|
|
||||||
|
Service exposed by the integration:
|
||||||
|
|
||||||
|
- `victron_mk2_mqtt.set_remote_panel_state`
|
||||||
|
|
||||||
|
Example service call:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
service: victron_mk2_mqtt.set_remote_panel_state
|
||||||
|
data:
|
||||||
|
mode: on
|
||||||
|
current_limit: 16.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Home Assistant MQTT-Only Dashboard (No Duplicate Entities)
|
||||||
|
|
||||||
|
If you want the same control/telemetry experience but only via MQTT (without duplicate
|
||||||
|
entities from discovery/custom integrations), use the packaged Home Assistant files:
|
||||||
|
|
||||||
|
- MQTT entity + control package: `homeassistant/packages/invertergui_mqtt.yaml`
|
||||||
|
- Lovelace dashboard: `homeassistant/dashboards/invertergui_mqtt_dashboard.yaml`
|
||||||
|
|
||||||
|
The package assumes default topics (`invertergui/updates`, `invertergui/settings/set`,
|
||||||
|
`invertergui/settings/status`). If you use custom MQTT topics, update those values in
|
||||||
|
`homeassistant/packages/invertergui_mqtt.yaml`.
|
||||||
|
|
||||||
|
Recommended for this mode:
|
||||||
|
|
||||||
|
- Disable MQTT discovery output from `invertergui` (`--mqtt.ha.enabled=false`)
|
||||||
|
- Do not enable the `victron_mk2_mqtt` custom component at the same time
|
||||||
|
|
||||||
|
1. Ensure HA packages are enabled (if not already):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
homeassistant:
|
||||||
|
packages: !include_dir_named packages
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Copy package file to your HA config:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<home-assistant-config>/packages/invertergui_mqtt.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Copy dashboard file to your HA config:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<home-assistant-config>/dashboards/invertergui_mqtt_dashboard.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Register the dashboard (YAML mode example):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
lovelace:
|
||||||
|
mode: storage
|
||||||
|
dashboards:
|
||||||
|
invertergui-victron:
|
||||||
|
mode: yaml
|
||||||
|
title: Victron MQTT
|
||||||
|
icon: mdi:flash
|
||||||
|
show_in_sidebar: true
|
||||||
|
filename: dashboards/invertergui_mqtt_dashboard.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Restart Home Assistant.
|
||||||
|
|
||||||
## TTY Device
|
## TTY Device
|
||||||
|
|
||||||
The intertergui application makes use of a serial tty device to monitor the Multiplus.
|
The intertergui application makes use of a serial tty device to monitor the Multiplus.
|
||||||
|
|||||||
125
custom_components/victron_mk2_mqtt/__init__.py
Normal file
125
custom_components/victron_mk2_mqtt/__init__.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""Home Assistant integration for invertergui MQTT topics."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import config_validation as cv, discovery
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_CURRENT_LIMIT,
|
||||||
|
ATTR_MODE,
|
||||||
|
CONF_COMMAND_TOPIC,
|
||||||
|
CONF_STATE_TOPIC,
|
||||||
|
CONF_STATUS_TOPIC,
|
||||||
|
CONF_TOPIC_ROOT,
|
||||||
|
DATA_BRIDGE,
|
||||||
|
DEFAULT_COMMAND_TOPIC,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_STATE_TOPIC,
|
||||||
|
DEFAULT_STATUS_TOPIC,
|
||||||
|
DEFAULT_TOPIC_ROOT,
|
||||||
|
DOMAIN,
|
||||||
|
PANEL_MODES,
|
||||||
|
PLATFORMS,
|
||||||
|
SERVICE_SET_REMOTE_PANEL_STATE,
|
||||||
|
)
|
||||||
|
from .coordinator import VictronMqttBridge
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_STATE_TOPIC, default=DEFAULT_STATE_TOPIC): cv.string,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_COMMAND_TOPIC, default=DEFAULT_COMMAND_TOPIC
|
||||||
|
): cv.string,
|
||||||
|
vol.Optional(CONF_STATUS_TOPIC, default=DEFAULT_STATUS_TOPIC): cv.string,
|
||||||
|
vol.Optional(CONF_TOPIC_ROOT): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
SERVICE_SET_REMOTE_PANEL_STATE_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_MODE): vol.In(PANEL_MODES),
|
||||||
|
vol.Optional(ATTR_CURRENT_LIMIT): vol.Coerce(float),
|
||||||
|
},
|
||||||
|
extra=vol.PREVENT_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mqtt_topic_root(topic: str) -> str:
|
||||||
|
"""Match invertergui MQTT root behavior."""
|
||||||
|
cleaned = topic.strip().strip("/")
|
||||||
|
if not cleaned:
|
||||||
|
return DEFAULT_TOPIC_ROOT
|
||||||
|
if cleaned.endswith("/updates"):
|
||||||
|
root = cleaned[: -len("/updates")]
|
||||||
|
if root:
|
||||||
|
return root
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up Victron MK2 MQTT integration from YAML."""
|
||||||
|
conf = config.get(DOMAIN)
|
||||||
|
if conf is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
setup_conf: dict[str, Any] = dict(conf)
|
||||||
|
if not setup_conf.get(CONF_TOPIC_ROOT):
|
||||||
|
setup_conf[CONF_TOPIC_ROOT] = mqtt_topic_root(setup_conf[CONF_STATE_TOPIC])
|
||||||
|
|
||||||
|
bridge = VictronMqttBridge(hass, setup_conf)
|
||||||
|
await bridge.async_setup()
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][DATA_BRIDGE] = bridge
|
||||||
|
|
||||||
|
await _register_services(hass, bridge)
|
||||||
|
|
||||||
|
for platform in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
discovery.async_load_platform(hass, platform, DOMAIN, {}, config)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _register_services(hass: HomeAssistant, bridge: VictronMqttBridge) -> None:
|
||||||
|
"""Register integration services."""
|
||||||
|
if hass.services.has_service(DOMAIN, SERVICE_SET_REMOTE_PANEL_STATE):
|
||||||
|
return
|
||||||
|
|
||||||
|
async def handle_set_remote_panel_state(call: ServiceCall) -> None:
|
||||||
|
mode = call.data.get(ATTR_MODE)
|
||||||
|
current_limit = call.data.get(ATTR_CURRENT_LIMIT)
|
||||||
|
|
||||||
|
if mode is None and current_limit is None:
|
||||||
|
raise HomeAssistantError("Provide at least one of mode or current_limit")
|
||||||
|
if current_limit is not None and current_limit < 0:
|
||||||
|
raise HomeAssistantError("current_limit must be >= 0")
|
||||||
|
|
||||||
|
payload: dict[str, Any] = {"kind": "panel_state"}
|
||||||
|
if mode is not None:
|
||||||
|
payload["switch"] = mode
|
||||||
|
if current_limit is not None:
|
||||||
|
payload["current_limit"] = float(current_limit)
|
||||||
|
|
||||||
|
await bridge.async_publish_command(payload)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_REMOTE_PANEL_STATE,
|
||||||
|
handle_set_remote_panel_state,
|
||||||
|
schema=SERVICE_SET_REMOTE_PANEL_STATE_SCHEMA,
|
||||||
|
)
|
||||||
48
custom_components/victron_mk2_mqtt/binary_sensor.py
Normal file
48
custom_components/victron_mk2_mqtt/binary_sensor.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Binary sensors for Victron MK2 MQTT integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DATA_BRIDGE, DOMAIN
|
||||||
|
from .coordinator import VictronMqttBridge
|
||||||
|
from .entity import VictronMqttEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict[str, Any],
|
||||||
|
async_add_entities,
|
||||||
|
discovery_info: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Victron binary sensors."""
|
||||||
|
bridge: VictronMqttBridge = hass.data[DOMAIN][DATA_BRIDGE]
|
||||||
|
async_add_entities([VictronDataValidBinarySensor(bridge)])
|
||||||
|
|
||||||
|
|
||||||
|
class VictronDataValidBinarySensor(VictronMqttEntity, BinarySensorEntity):
|
||||||
|
"""MQTT data validity sensor."""
|
||||||
|
|
||||||
|
_attr_name = "Data Valid"
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
_attr_icon = "mdi:check-network-outline"
|
||||||
|
|
||||||
|
def __init__(self, bridge: VictronMqttBridge) -> None:
|
||||||
|
super().__init__(bridge)
|
||||||
|
self._attr_unique_id = f"{bridge.topic_root}_data_valid"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
value = self.bridge.metric("Valid")
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return value != 0
|
||||||
|
if isinstance(value, str):
|
||||||
|
normalized = value.strip().lower()
|
||||||
|
return normalized in {"1", "true", "on", "yes"}
|
||||||
|
return False
|
||||||
37
custom_components/victron_mk2_mqtt/const.py
Normal file
37
custom_components/victron_mk2_mqtt/const.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""Constants for the Victron MK2 MQTT integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
DOMAIN = "victron_mk2_mqtt"
|
||||||
|
|
||||||
|
CONF_STATE_TOPIC = "state_topic"
|
||||||
|
CONF_COMMAND_TOPIC = "command_topic"
|
||||||
|
CONF_STATUS_TOPIC = "status_topic"
|
||||||
|
CONF_TOPIC_ROOT = "topic_root"
|
||||||
|
CONF_NAME = "name"
|
||||||
|
|
||||||
|
DEFAULT_STATE_TOPIC = "invertergui/updates"
|
||||||
|
DEFAULT_COMMAND_TOPIC = "invertergui/settings/set"
|
||||||
|
DEFAULT_STATUS_TOPIC = "invertergui/settings/status"
|
||||||
|
DEFAULT_TOPIC_ROOT = "invertergui"
|
||||||
|
DEFAULT_NAME = "Victron Inverter"
|
||||||
|
|
||||||
|
PLATFORMS = ("sensor", "binary_sensor", "select", "number", "switch")
|
||||||
|
|
||||||
|
DATA_BRIDGE = "bridge"
|
||||||
|
|
||||||
|
ATTR_MODE = "mode"
|
||||||
|
ATTR_CURRENT_LIMIT = "current_limit"
|
||||||
|
|
||||||
|
SERVICE_SET_REMOTE_PANEL_STATE = "set_remote_panel_state"
|
||||||
|
|
||||||
|
PANEL_MODE_CHARGER_ONLY = "charger_only"
|
||||||
|
PANEL_MODE_INVERTER_ONLY = "inverter_only"
|
||||||
|
PANEL_MODE_ON = "on"
|
||||||
|
PANEL_MODE_OFF = "off"
|
||||||
|
PANEL_MODES = (
|
||||||
|
PANEL_MODE_CHARGER_ONLY,
|
||||||
|
PANEL_MODE_INVERTER_ONLY,
|
||||||
|
PANEL_MODE_ON,
|
||||||
|
PANEL_MODE_OFF,
|
||||||
|
)
|
||||||
232
custom_components/victron_mk2_mqtt/coordinator.py
Normal file
232
custom_components/victron_mk2_mqtt/coordinator.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
"""MQTT bridge for Victron MK2 integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components import mqtt
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_COMMAND_TOPIC,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_STATE_TOPIC,
|
||||||
|
CONF_STATUS_TOPIC,
|
||||||
|
CONF_TOPIC_ROOT,
|
||||||
|
DOMAIN,
|
||||||
|
PANEL_MODES,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VictronMqttBridge:
|
||||||
|
"""Maintain MQTT state and command publishing for Victron entities."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||||
|
self.hass = hass
|
||||||
|
|
||||||
|
self.name: str = config[CONF_NAME]
|
||||||
|
self.state_topic: str = config[CONF_STATE_TOPIC]
|
||||||
|
self.command_topic: str = config[CONF_COMMAND_TOPIC]
|
||||||
|
self.status_topic: str = config[CONF_STATUS_TOPIC]
|
||||||
|
self.topic_root: str = config[CONF_TOPIC_ROOT]
|
||||||
|
|
||||||
|
self.panel_mode_state_topic = f"{self.topic_root}/homeassistant/remote_panel_mode/state"
|
||||||
|
self.current_limit_state_topic = (
|
||||||
|
f"{self.topic_root}/homeassistant/remote_panel_current_limit/state"
|
||||||
|
)
|
||||||
|
self.standby_state_topic = f"{self.topic_root}/homeassistant/remote_panel_standby/state"
|
||||||
|
|
||||||
|
self.telemetry: dict[str, Any] = {}
|
||||||
|
self.panel_mode: str | None = None
|
||||||
|
self.current_limit: float | None = None
|
||||||
|
self.standby: bool | None = None
|
||||||
|
self.last_error: str | None = None
|
||||||
|
|
||||||
|
self._listeners: set[Callable[[], None]] = set()
|
||||||
|
self._unsubscribers: list[Callable[[], None]] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return shared Home Assistant device metadata."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self.topic_root)},
|
||||||
|
name=self.name,
|
||||||
|
manufacturer="Victron Energy",
|
||||||
|
model="VE.Bus via invertergui MQTT",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_setup(self) -> None:
|
||||||
|
"""Subscribe to required MQTT topics."""
|
||||||
|
_LOGGER.info(
|
||||||
|
"Subscribing Victron MQTT bridge topics state=%s command=%s status=%s",
|
||||||
|
self.state_topic,
|
||||||
|
self.command_topic,
|
||||||
|
self.status_topic,
|
||||||
|
)
|
||||||
|
self._unsubscribers.append(
|
||||||
|
await mqtt.async_subscribe(
|
||||||
|
self.hass, self.state_topic, self._handle_state_message, qos=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._unsubscribers.append(
|
||||||
|
await mqtt.async_subscribe(
|
||||||
|
self.hass,
|
||||||
|
self.panel_mode_state_topic,
|
||||||
|
self._handle_panel_mode_message,
|
||||||
|
qos=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._unsubscribers.append(
|
||||||
|
await mqtt.async_subscribe(
|
||||||
|
self.hass,
|
||||||
|
self.current_limit_state_topic,
|
||||||
|
self._handle_current_limit_message,
|
||||||
|
qos=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._unsubscribers.append(
|
||||||
|
await mqtt.async_subscribe(
|
||||||
|
self.hass, self.standby_state_topic, self._handle_standby_message, qos=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self.status_topic:
|
||||||
|
self._unsubscribers.append(
|
||||||
|
await mqtt.async_subscribe(
|
||||||
|
self.hass, self.status_topic, self._handle_status_message, qos=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_shutdown(self) -> None:
|
||||||
|
"""Unsubscribe all MQTT subscriptions."""
|
||||||
|
while self._unsubscribers:
|
||||||
|
unsub = self._unsubscribers.pop()
|
||||||
|
unsub()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_listener(self, listener: Callable[[], None]) -> Callable[[], None]:
|
||||||
|
"""Register a state listener."""
|
||||||
|
self._listeners.add(listener)
|
||||||
|
|
||||||
|
def remove() -> None:
|
||||||
|
self._listeners.discard(listener)
|
||||||
|
|
||||||
|
return remove
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _notify_listeners(self) -> None:
|
||||||
|
"""Notify all entities that state changed."""
|
||||||
|
for listener in tuple(self._listeners):
|
||||||
|
listener()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _payload_text(payload: Any) -> str:
|
||||||
|
if isinstance(payload, bytes):
|
||||||
|
return payload.decode("utf-8", errors="ignore")
|
||||||
|
if isinstance(payload, str):
|
||||||
|
return payload
|
||||||
|
return str(payload)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_state_message(self, msg: Any) -> None:
|
||||||
|
raw_payload = self._payload_text(msg.payload)
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw_payload)
|
||||||
|
except json.JSONDecodeError as err:
|
||||||
|
_LOGGER.warning("Ignoring invalid state JSON from %s: %s", msg.topic, err)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
_LOGGER.warning("Ignoring state payload from %s: expected object", msg.topic)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.telemetry = payload
|
||||||
|
self._notify_listeners()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_panel_mode_message(self, msg: Any) -> None:
|
||||||
|
mode = self._payload_text(msg.payload).strip().lower()
|
||||||
|
if mode not in PANEL_MODES:
|
||||||
|
_LOGGER.debug("Ignoring unknown panel mode payload %r", msg.payload)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.panel_mode = mode
|
||||||
|
self._notify_listeners()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_current_limit_message(self, msg: Any) -> None:
|
||||||
|
payload = self._payload_text(msg.payload).strip()
|
||||||
|
if not payload:
|
||||||
|
self.current_limit = None
|
||||||
|
self._notify_listeners()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.current_limit = float(payload)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.debug("Ignoring invalid current limit payload %r", msg.payload)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._notify_listeners()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_standby_message(self, msg: Any) -> None:
|
||||||
|
value = self._payload_text(msg.payload).strip().lower()
|
||||||
|
if value in {"on", "1", "true"}:
|
||||||
|
self.standby = True
|
||||||
|
elif value in {"off", "0", "false"}:
|
||||||
|
self.standby = False
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Ignoring invalid standby payload %r", msg.payload)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._notify_listeners()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_status_message(self, msg: Any) -> None:
|
||||||
|
raw_payload = self._payload_text(msg.payload)
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw_payload)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
if payload.get("status") == "error":
|
||||||
|
err = payload.get("error")
|
||||||
|
self.last_error = str(err) if err is not None else "unknown error"
|
||||||
|
else:
|
||||||
|
self.last_error = None
|
||||||
|
self._notify_listeners()
|
||||||
|
|
||||||
|
async def async_publish_command(self, payload: dict[str, Any]) -> None:
|
||||||
|
"""Publish a control command payload to invertergui command topic."""
|
||||||
|
if not self.command_topic:
|
||||||
|
raise HomeAssistantError("MQTT command topic is not configured")
|
||||||
|
|
||||||
|
mqtt.async_publish(
|
||||||
|
self.hass,
|
||||||
|
self.command_topic,
|
||||||
|
json.dumps(payload, separators=(",", ":")),
|
||||||
|
qos=1,
|
||||||
|
retain=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def metric(self, key: str) -> Any:
|
||||||
|
"""Read a telemetry key."""
|
||||||
|
return self.telemetry.get(key)
|
||||||
|
|
||||||
|
def metric_float(self, key: str) -> float | None:
|
||||||
|
"""Read and coerce telemetry value to float."""
|
||||||
|
value = self.metric(key)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
29
custom_components/victron_mk2_mqtt/entity.py
Normal file
29
custom_components/victron_mk2_mqtt/entity.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"""Shared entity base for Victron MK2 MQTT."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .coordinator import VictronMqttBridge
|
||||||
|
|
||||||
|
|
||||||
|
class VictronMqttEntity(Entity):
|
||||||
|
"""Base entity bound to shared MQTT bridge."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, bridge: VictronMqttBridge) -> None:
|
||||||
|
self.bridge = bridge
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the shared device info."""
|
||||||
|
return self.bridge.device_info
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Register for coordinator updates."""
|
||||||
|
self.async_on_remove(self.bridge.async_add_listener(self._handle_bridge_update))
|
||||||
|
|
||||||
|
def _handle_bridge_update(self) -> None:
|
||||||
|
self.async_write_ha_state()
|
||||||
14
custom_components/victron_mk2_mqtt/manifest.json
Normal file
14
custom_components/victron_mk2_mqtt/manifest.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"domain": "victron_mk2_mqtt",
|
||||||
|
"name": "Victron MK2 MQTT",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"documentation": "https://git.coadcorp.com/nathan/invertergui",
|
||||||
|
"issue_tracker": "https://git.coadcorp.com/nathan/invertergui/issues",
|
||||||
|
"dependencies": [
|
||||||
|
"mqtt"
|
||||||
|
],
|
||||||
|
"codeowners": [
|
||||||
|
"@nathan"
|
||||||
|
],
|
||||||
|
"iot_class": "local_push"
|
||||||
|
}
|
||||||
54
custom_components/victron_mk2_mqtt/number.py
Normal file
54
custom_components/victron_mk2_mqtt/number.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""Number entities for Victron MK2 MQTT integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
|
||||||
|
from homeassistant.const import UnitOfElectricCurrent
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DATA_BRIDGE, DOMAIN
|
||||||
|
from .coordinator import VictronMqttBridge
|
||||||
|
from .entity import VictronMqttEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict[str, Any],
|
||||||
|
async_add_entities,
|
||||||
|
discovery_info: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Victron number entities."""
|
||||||
|
bridge: VictronMqttBridge = hass.data[DOMAIN][DATA_BRIDGE]
|
||||||
|
async_add_entities([VictronRemotePanelCurrentLimitNumber(bridge)])
|
||||||
|
|
||||||
|
|
||||||
|
class VictronRemotePanelCurrentLimitNumber(VictronMqttEntity, NumberEntity):
|
||||||
|
"""Remote panel AC input current limit."""
|
||||||
|
|
||||||
|
_attr_name = "Remote Panel Current Limit"
|
||||||
|
_attr_icon = "mdi:current-ac"
|
||||||
|
_attr_native_min_value = 0.0
|
||||||
|
_attr_native_max_value = 100.0
|
||||||
|
_attr_native_step = 0.1
|
||||||
|
_attr_mode = NumberMode.BOX
|
||||||
|
_attr_device_class = NumberDeviceClass.CURRENT
|
||||||
|
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
|
||||||
|
|
||||||
|
def __init__(self, bridge: VictronMqttBridge) -> None:
|
||||||
|
super().__init__(bridge)
|
||||||
|
self._attr_unique_id = f"{bridge.topic_root}_remote_panel_current_limit"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
return self.bridge.current_limit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
return bool(self.bridge.command_topic)
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
await self.bridge.async_publish_command(
|
||||||
|
{"kind": "panel_state", "current_limit": float(value)}
|
||||||
|
)
|
||||||
46
custom_components/victron_mk2_mqtt/select.py
Normal file
46
custom_components/victron_mk2_mqtt/select.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Select entities for Victron MK2 MQTT integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DATA_BRIDGE, DOMAIN, PANEL_MODES
|
||||||
|
from .coordinator import VictronMqttBridge
|
||||||
|
from .entity import VictronMqttEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict[str, Any],
|
||||||
|
async_add_entities,
|
||||||
|
discovery_info: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Victron select entities."""
|
||||||
|
bridge: VictronMqttBridge = hass.data[DOMAIN][DATA_BRIDGE]
|
||||||
|
async_add_entities([VictronRemotePanelModeSelect(bridge)])
|
||||||
|
|
||||||
|
|
||||||
|
class VictronRemotePanelModeSelect(VictronMqttEntity, SelectEntity):
|
||||||
|
"""Remote panel mode select."""
|
||||||
|
|
||||||
|
_attr_name = "Remote Panel Mode"
|
||||||
|
_attr_options = list(PANEL_MODES)
|
||||||
|
_attr_icon = "mdi:transmission-tower-export"
|
||||||
|
|
||||||
|
def __init__(self, bridge: VictronMqttBridge) -> None:
|
||||||
|
super().__init__(bridge)
|
||||||
|
self._attr_unique_id = f"{bridge.topic_root}_remote_panel_mode"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
return self.bridge.panel_mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
return bool(self.bridge.command_topic)
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
await self.bridge.async_publish_command({"kind": "panel_state", "switch": option})
|
||||||
148
custom_components/victron_mk2_mqtt/sensor.py
Normal file
148
custom_components/victron_mk2_mqtt/sensor.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"""Sensor entities for Victron MK2 MQTT integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import SensorEntity, SensorStateClass
|
||||||
|
from homeassistant.const import (
|
||||||
|
EntityCategory,
|
||||||
|
PERCENTAGE,
|
||||||
|
UnitOfElectricCurrent,
|
||||||
|
UnitOfElectricPotential,
|
||||||
|
UnitOfFrequency,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DATA_BRIDGE, DOMAIN
|
||||||
|
from .coordinator import VictronMqttBridge
|
||||||
|
from .entity import VictronMqttEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MetricDescription:
|
||||||
|
"""Description for a telemetry-backed sensor."""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
name: str
|
||||||
|
value_fn: Callable[[VictronMqttBridge], Any]
|
||||||
|
unit: str | None = None
|
||||||
|
state_class: SensorStateClass | None = SensorStateClass.MEASUREMENT
|
||||||
|
entity_category: EntityCategory | None = None
|
||||||
|
|
||||||
|
|
||||||
|
METRICS: tuple[MetricDescription, ...] = (
|
||||||
|
MetricDescription(
|
||||||
|
key="battery_voltage",
|
||||||
|
name="Battery Voltage",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("BatVoltage"),
|
||||||
|
unit=UnitOfElectricPotential.VOLT,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="battery_current",
|
||||||
|
name="Battery Current",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("BatCurrent"),
|
||||||
|
unit=UnitOfElectricCurrent.AMPERE,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="battery_charge",
|
||||||
|
name="Battery Charge",
|
||||||
|
value_fn=lambda bridge: (
|
||||||
|
bridge.metric_float("ChargeState") * 100.0
|
||||||
|
if bridge.metric_float("ChargeState") is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
unit=PERCENTAGE,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="input_voltage",
|
||||||
|
name="Input Voltage",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("InVoltage"),
|
||||||
|
unit=UnitOfElectricPotential.VOLT,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="input_current",
|
||||||
|
name="Input Current",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("InCurrent"),
|
||||||
|
unit=UnitOfElectricCurrent.AMPERE,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="input_frequency",
|
||||||
|
name="Input Frequency",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("InFrequency"),
|
||||||
|
unit=UnitOfFrequency.HERTZ,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="output_voltage",
|
||||||
|
name="Output Voltage",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("OutVoltage"),
|
||||||
|
unit=UnitOfElectricPotential.VOLT,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="output_current",
|
||||||
|
name="Output Current",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("OutCurrent"),
|
||||||
|
unit=UnitOfElectricCurrent.AMPERE,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="output_frequency",
|
||||||
|
name="Output Frequency",
|
||||||
|
value_fn=lambda bridge: bridge.metric_float("OutFrequency"),
|
||||||
|
unit=UnitOfFrequency.HERTZ,
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="input_power",
|
||||||
|
name="Input Power",
|
||||||
|
value_fn=lambda bridge: _product(bridge.metric_float("InVoltage"), bridge.metric_float("InCurrent")),
|
||||||
|
unit="VA",
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="output_power",
|
||||||
|
name="Output Power",
|
||||||
|
value_fn=lambda bridge: _product(bridge.metric_float("OutVoltage"), bridge.metric_float("OutCurrent")),
|
||||||
|
unit="VA",
|
||||||
|
),
|
||||||
|
MetricDescription(
|
||||||
|
key="last_command_error",
|
||||||
|
name="Last Command Error",
|
||||||
|
value_fn=lambda bridge: bridge.last_error,
|
||||||
|
state_class=None,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _product(a: float | None, b: float | None) -> float | None:
|
||||||
|
if a is None or b is None:
|
||||||
|
return None
|
||||||
|
return a * b
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict[str, Any],
|
||||||
|
async_add_entities,
|
||||||
|
discovery_info: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Victron telemetry sensors."""
|
||||||
|
bridge: VictronMqttBridge = hass.data[DOMAIN][DATA_BRIDGE]
|
||||||
|
async_add_entities(VictronMetricSensor(bridge, metric) for metric in METRICS)
|
||||||
|
|
||||||
|
|
||||||
|
class VictronMetricSensor(VictronMqttEntity, SensorEntity):
|
||||||
|
"""Generic telemetry sensor."""
|
||||||
|
|
||||||
|
def __init__(self, bridge: VictronMqttBridge, description: MetricDescription) -> None:
|
||||||
|
super().__init__(bridge)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{bridge.topic_root}_{description.key}"
|
||||||
|
self._attr_name = description.name
|
||||||
|
self._attr_native_unit_of_measurement = description.unit
|
||||||
|
self._attr_state_class = description.state_class
|
||||||
|
self._attr_entity_category = description.entity_category
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self):
|
||||||
|
return self.entity_description.value_fn(self.bridge)
|
||||||
27
custom_components/victron_mk2_mqtt/services.yaml
Normal file
27
custom_components/victron_mk2_mqtt/services.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
set_remote_panel_state:
|
||||||
|
name: Set Remote Panel State
|
||||||
|
description: Set the remote panel mode and/or AC input current limit over MQTT.
|
||||||
|
fields:
|
||||||
|
mode:
|
||||||
|
name: Mode
|
||||||
|
description: Remote panel mode.
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- charger_only
|
||||||
|
- inverter_only
|
||||||
|
- on
|
||||||
|
- off
|
||||||
|
current_limit:
|
||||||
|
name: Current Limit
|
||||||
|
description: AC input current limit in amps.
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
step: 0.1
|
||||||
|
unit_of_measurement: A
|
||||||
|
mode: box
|
||||||
48
custom_components/victron_mk2_mqtt/switch.py
Normal file
48
custom_components/victron_mk2_mqtt/switch.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Switch entities for Victron MK2 MQTT integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DATA_BRIDGE, DOMAIN
|
||||||
|
from .coordinator import VictronMqttBridge
|
||||||
|
from .entity import VictronMqttEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict[str, Any],
|
||||||
|
async_add_entities,
|
||||||
|
discovery_info: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Victron switch entities."""
|
||||||
|
bridge: VictronMqttBridge = hass.data[DOMAIN][DATA_BRIDGE]
|
||||||
|
async_add_entities([VictronRemotePanelStandbySwitch(bridge)])
|
||||||
|
|
||||||
|
|
||||||
|
class VictronRemotePanelStandbySwitch(VictronMqttEntity, SwitchEntity):
|
||||||
|
"""Remote panel standby switch."""
|
||||||
|
|
||||||
|
_attr_name = "Remote Panel Standby"
|
||||||
|
_attr_icon = "mdi:power-sleep"
|
||||||
|
|
||||||
|
def __init__(self, bridge: VictronMqttBridge) -> None:
|
||||||
|
super().__init__(bridge)
|
||||||
|
self._attr_unique_id = f"{bridge.topic_root}_remote_panel_standby"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
return bool(self.bridge.standby)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
return bool(self.bridge.command_topic)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
await self.bridge.async_publish_command({"kind": "standby", "standby": True})
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
await self.bridge.async_publish_command({"kind": "standby", "standby": False})
|
||||||
120
homeassistant/dashboards/invertergui_mqtt_dashboard.yaml
Normal file
120
homeassistant/dashboards/invertergui_mqtt_dashboard.yaml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
title: Victron Inverter MQTT
|
||||||
|
views:
|
||||||
|
- title: Inverter
|
||||||
|
path: victron-inverter
|
||||||
|
icon: mdi:flash
|
||||||
|
badges:
|
||||||
|
- entity: binary_sensor.victron_online
|
||||||
|
- entity: binary_sensor.victron_data_valid
|
||||||
|
- entity: sensor.victron_last_command_status
|
||||||
|
cards:
|
||||||
|
- type: vertical-stack
|
||||||
|
cards:
|
||||||
|
- type: markdown
|
||||||
|
content: |
|
||||||
|
## Remote Panel Control
|
||||||
|
Mode and current limit are published together over MQTT, matching `set_remote_panel_state`.
|
||||||
|
- type: entities
|
||||||
|
title: Current Remote State
|
||||||
|
state_color: true
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: select.victron_remote_panel_mode
|
||||||
|
name: Mode
|
||||||
|
- entity: number.victron_remote_panel_current_limit
|
||||||
|
name: AC Input Current Limit
|
||||||
|
- entity: switch.victron_remote_panel_standby
|
||||||
|
name: Prevent Sleep While Off
|
||||||
|
- entity: sensor.victron_last_command_error
|
||||||
|
name: Last Command Error
|
||||||
|
- type: entities
|
||||||
|
title: Apply Mode + Current Limit
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: input_select.victron_remote_panel_mode_target
|
||||||
|
name: Target Mode
|
||||||
|
- entity: input_number.victron_remote_panel_current_limit_target
|
||||||
|
name: Target Current Limit
|
||||||
|
- entity: script.victron_mqtt_set_remote_panel_state
|
||||||
|
name: Apply Mode + Current Limit
|
||||||
|
- type: entities
|
||||||
|
title: Apply Standby
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: input_boolean.victron_remote_panel_standby_target
|
||||||
|
name: Target Standby
|
||||||
|
- entity: script.victron_mqtt_set_remote_panel_standby
|
||||||
|
name: Apply Standby
|
||||||
|
|
||||||
|
- type: grid
|
||||||
|
columns: 3
|
||||||
|
square: false
|
||||||
|
cards:
|
||||||
|
- type: entities
|
||||||
|
title: Output
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: sensor.victron_output_current
|
||||||
|
name: Output Current
|
||||||
|
- entity: sensor.victron_output_voltage
|
||||||
|
name: Output Voltage
|
||||||
|
- entity: sensor.victron_output_frequency
|
||||||
|
name: Output Frequency
|
||||||
|
- entity: sensor.victron_output_power
|
||||||
|
name: Output Power
|
||||||
|
- type: entities
|
||||||
|
title: Input
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: sensor.victron_input_current
|
||||||
|
name: Input Current
|
||||||
|
- entity: sensor.victron_input_voltage
|
||||||
|
name: Input Voltage
|
||||||
|
- entity: sensor.victron_input_frequency
|
||||||
|
name: Input Frequency
|
||||||
|
- entity: sensor.victron_input_power
|
||||||
|
name: Input Power
|
||||||
|
- entity: sensor.victron_input_minus_output_power
|
||||||
|
name: Input - Output Power
|
||||||
|
- type: entities
|
||||||
|
title: Battery
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: sensor.victron_battery_current
|
||||||
|
name: Battery Current
|
||||||
|
- entity: sensor.victron_battery_voltage
|
||||||
|
name: Battery Voltage
|
||||||
|
- entity: sensor.victron_battery_power
|
||||||
|
name: Battery Power
|
||||||
|
- entity: sensor.victron_battery_charge
|
||||||
|
name: Battery Charge
|
||||||
|
|
||||||
|
- type: entities
|
||||||
|
title: LED Status
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: sensor.victron_led_mains
|
||||||
|
name: Mains
|
||||||
|
- entity: sensor.victron_led_absorb
|
||||||
|
name: Absorb
|
||||||
|
- entity: sensor.victron_led_bulk
|
||||||
|
name: Bulk
|
||||||
|
- entity: sensor.victron_led_float
|
||||||
|
name: Float
|
||||||
|
- entity: sensor.victron_led_inverter
|
||||||
|
name: Inverter
|
||||||
|
- entity: sensor.victron_led_overload
|
||||||
|
name: Overload
|
||||||
|
- entity: sensor.victron_led_low_battery
|
||||||
|
name: Low Battery
|
||||||
|
- entity: sensor.victron_led_over_temp
|
||||||
|
name: Over Temperature
|
||||||
|
|
||||||
|
- type: history-graph
|
||||||
|
title: Power (Last 2 Hours)
|
||||||
|
hours_to_show: 2
|
||||||
|
refresh_interval: 60
|
||||||
|
entities:
|
||||||
|
- entity: sensor.victron_input_power
|
||||||
|
- entity: sensor.victron_output_power
|
||||||
|
- entity: sensor.victron_battery_power
|
||||||
350
homeassistant/packages/invertergui_mqtt.yaml
Normal file
350
homeassistant/packages/invertergui_mqtt.yaml
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# MQTT-only Home Assistant package for invertergui.
|
||||||
|
# This avoids duplicate entities from MQTT auto-discovery/custom integration.
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - invertergui started with MQTT publishing enabled.
|
||||||
|
# - invertergui MQTT discovery disabled (`--mqtt.ha.enabled=false`) when using this package.
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
sensor:
|
||||||
|
- name: Victron Battery Voltage
|
||||||
|
unique_id: invertergui_mqtt_battery_voltage
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.BatVoltage }}"
|
||||||
|
unit_of_measurement: V
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Battery Current
|
||||||
|
unique_id: invertergui_mqtt_battery_current
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.BatCurrent }}"
|
||||||
|
unit_of_measurement: A
|
||||||
|
device_class: current
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Battery Charge
|
||||||
|
unique_id: invertergui_mqtt_battery_charge
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ ((value_json.ChargeState | float(0)) * 100) | round(1) }}"
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
device_class: battery
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Input Voltage
|
||||||
|
unique_id: invertergui_mqtt_input_voltage
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.InVoltage }}"
|
||||||
|
unit_of_measurement: V
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Input Current
|
||||||
|
unique_id: invertergui_mqtt_input_current
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.InCurrent }}"
|
||||||
|
unit_of_measurement: A
|
||||||
|
device_class: current
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Input Frequency
|
||||||
|
unique_id: invertergui_mqtt_input_frequency
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.InFrequency }}"
|
||||||
|
unit_of_measurement: Hz
|
||||||
|
device_class: frequency
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Output Voltage
|
||||||
|
unique_id: invertergui_mqtt_output_voltage
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.OutVoltage }}"
|
||||||
|
unit_of_measurement: V
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Output Current
|
||||||
|
unique_id: invertergui_mqtt_output_current
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.OutCurrent }}"
|
||||||
|
unit_of_measurement: A
|
||||||
|
device_class: current
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Output Frequency
|
||||||
|
unique_id: invertergui_mqtt_output_frequency
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.OutFrequency }}"
|
||||||
|
unit_of_measurement: Hz
|
||||||
|
device_class: frequency
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Input Power
|
||||||
|
unique_id: invertergui_mqtt_input_power
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ ((value_json.InVoltage | float(0)) * (value_json.InCurrent | float(0))) | round(1) }}"
|
||||||
|
unit_of_measurement: VA
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Output Power
|
||||||
|
unique_id: invertergui_mqtt_output_power
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ ((value_json.OutVoltage | float(0)) * (value_json.OutCurrent | float(0))) | round(1) }}"
|
||||||
|
unit_of_measurement: VA
|
||||||
|
state_class: measurement
|
||||||
|
- name: Victron Last Command Status
|
||||||
|
unique_id: invertergui_mqtt_last_command_status
|
||||||
|
state_topic: invertergui/settings/status
|
||||||
|
value_template: "{{ value_json.status | default('unknown') }}"
|
||||||
|
icon: mdi:message-alert-outline
|
||||||
|
- name: Victron Last Command Error
|
||||||
|
unique_id: invertergui_mqtt_last_command_error
|
||||||
|
state_topic: invertergui/settings/status
|
||||||
|
value_template: >-
|
||||||
|
{% if value_json.status == 'error' %}
|
||||||
|
{{ value_json.error | default('unknown error') }}
|
||||||
|
{% else %}
|
||||||
|
none
|
||||||
|
{% endif %}
|
||||||
|
icon: mdi:alert-circle-outline
|
||||||
|
- name: Victron LED Mains
|
||||||
|
unique_id: invertergui_mqtt_led_mains
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['0'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:transmission-tower
|
||||||
|
- name: Victron LED Absorb
|
||||||
|
unique_id: invertergui_mqtt_led_absorb
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['1'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:water-outline
|
||||||
|
- name: Victron LED Bulk
|
||||||
|
unique_id: invertergui_mqtt_led_bulk
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['2'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:battery-plus
|
||||||
|
- name: Victron LED Float
|
||||||
|
unique_id: invertergui_mqtt_led_float
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['3'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:battery-heart-variant
|
||||||
|
- name: Victron LED Inverter
|
||||||
|
unique_id: invertergui_mqtt_led_inverter
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['4'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:power-plug
|
||||||
|
- name: Victron LED Overload
|
||||||
|
unique_id: invertergui_mqtt_led_overload
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['5'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:alert-octagon
|
||||||
|
- name: Victron LED Low Battery
|
||||||
|
unique_id: invertergui_mqtt_led_low_battery
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['6'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:battery-alert-variant-outline
|
||||||
|
- name: Victron LED Over Temp
|
||||||
|
unique_id: invertergui_mqtt_led_over_temp
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: >-
|
||||||
|
{% set leds = value_json.LEDs | default({}) %}
|
||||||
|
{% set v = leds['7'] | default(0) | int(0) %}
|
||||||
|
{% if v == 1 %}on{% elif v == 2 %}blink{% else %}off{% endif %}
|
||||||
|
icon: mdi:thermometer-alert
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- name: Victron Online
|
||||||
|
unique_id: invertergui_mqtt_online
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "ON"
|
||||||
|
payload_on: "ON"
|
||||||
|
payload_off: "OFF"
|
||||||
|
expire_after: 120
|
||||||
|
device_class: connectivity
|
||||||
|
- name: Victron Data Valid
|
||||||
|
unique_id: invertergui_mqtt_data_valid
|
||||||
|
state_topic: invertergui/updates
|
||||||
|
value_template: "{{ value_json.Valid }}"
|
||||||
|
payload_on: "true"
|
||||||
|
payload_off: "false"
|
||||||
|
entity_category: diagnostic
|
||||||
|
|
||||||
|
select:
|
||||||
|
- name: Victron Remote Panel Mode
|
||||||
|
unique_id: invertergui_mqtt_remote_panel_mode
|
||||||
|
state_topic: invertergui/homeassistant/remote_panel_mode/state
|
||||||
|
command_topic: invertergui/settings/set
|
||||||
|
command_template: '{"kind":"panel_state","switch":"{{ value }}"}'
|
||||||
|
options:
|
||||||
|
- charger_only
|
||||||
|
- inverter_only
|
||||||
|
- on
|
||||||
|
- off
|
||||||
|
icon: mdi:transmission-tower-export
|
||||||
|
|
||||||
|
number:
|
||||||
|
- name: Victron Remote Panel Current Limit
|
||||||
|
unique_id: invertergui_mqtt_remote_panel_current_limit
|
||||||
|
state_topic: invertergui/homeassistant/remote_panel_current_limit/state
|
||||||
|
command_topic: invertergui/settings/set
|
||||||
|
command_template: '{"kind":"panel_state","current_limit":{{ value | float(0) }}}'
|
||||||
|
unit_of_measurement: A
|
||||||
|
device_class: current
|
||||||
|
mode: box
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
step: 0.1
|
||||||
|
icon: mdi:current-ac
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- name: Victron Remote Panel Standby
|
||||||
|
unique_id: invertergui_mqtt_remote_panel_standby
|
||||||
|
state_topic: invertergui/homeassistant/remote_panel_standby/state
|
||||||
|
command_topic: invertergui/settings/set
|
||||||
|
payload_on: '{"kind":"standby","standby":true}'
|
||||||
|
payload_off: '{"kind":"standby","standby":false}'
|
||||||
|
state_on: "ON"
|
||||||
|
state_off: "OFF"
|
||||||
|
icon: mdi:power-sleep
|
||||||
|
|
||||||
|
input_select:
|
||||||
|
victron_remote_panel_mode_target:
|
||||||
|
name: Victron Target Mode
|
||||||
|
options:
|
||||||
|
- charger_only
|
||||||
|
- inverter_only
|
||||||
|
- on
|
||||||
|
- off
|
||||||
|
icon: mdi:transmission-tower-export
|
||||||
|
|
||||||
|
input_number:
|
||||||
|
victron_remote_panel_current_limit_target:
|
||||||
|
name: Victron Target Current Limit
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
step: 0.1
|
||||||
|
unit_of_measurement: A
|
||||||
|
mode: box
|
||||||
|
icon: mdi:current-ac
|
||||||
|
|
||||||
|
input_boolean:
|
||||||
|
victron_remote_panel_standby_target:
|
||||||
|
name: Victron Target Standby
|
||||||
|
icon: mdi:power-sleep
|
||||||
|
|
||||||
|
script:
|
||||||
|
victron_mqtt_set_remote_panel_state:
|
||||||
|
alias: Victron MQTT Set Remote Panel State
|
||||||
|
description: Set panel mode and current limit in one MQTT command.
|
||||||
|
mode: single
|
||||||
|
icon: mdi:send
|
||||||
|
sequence:
|
||||||
|
- service: mqtt.publish
|
||||||
|
data:
|
||||||
|
topic: invertergui/settings/set
|
||||||
|
qos: 1
|
||||||
|
payload: >-
|
||||||
|
{"kind":"panel_state","switch":"{{ states('input_select.victron_remote_panel_mode_target') }}","current_limit":{{ states('input_number.victron_remote_panel_current_limit_target') | float(0) | round(1) }}}
|
||||||
|
|
||||||
|
victron_mqtt_set_remote_panel_standby:
|
||||||
|
alias: Victron MQTT Set Remote Panel Standby
|
||||||
|
description: Set standby state from helper input.
|
||||||
|
mode: single
|
||||||
|
icon: mdi:send-circle
|
||||||
|
sequence:
|
||||||
|
- service: mqtt.publish
|
||||||
|
data:
|
||||||
|
topic: invertergui/settings/set
|
||||||
|
qos: 1
|
||||||
|
payload: >-
|
||||||
|
{"kind":"standby","standby":{% if is_state('input_boolean.victron_remote_panel_standby_target', 'on') %}true{% else %}false{% endif %}}
|
||||||
|
|
||||||
|
template:
|
||||||
|
- sensor:
|
||||||
|
- name: Victron Battery Power
|
||||||
|
unique_id: invertergui_mqtt_battery_power
|
||||||
|
unit_of_measurement: W
|
||||||
|
state_class: measurement
|
||||||
|
icon: mdi:battery-charging
|
||||||
|
state: >-
|
||||||
|
{{ ((states('sensor.victron_battery_voltage') | float(0)) * (states('sensor.victron_battery_current') | float(0))) | round(1) }}
|
||||||
|
- name: Victron Input Minus Output Power
|
||||||
|
unique_id: invertergui_mqtt_input_minus_output_power
|
||||||
|
unit_of_measurement: VA
|
||||||
|
state_class: measurement
|
||||||
|
icon: mdi:flash-triangle
|
||||||
|
state: >-
|
||||||
|
{{ (states('sensor.victron_input_power') | float(0) - states('sensor.victron_output_power') | float(0)) | round(1) }}
|
||||||
|
|
||||||
|
automation:
|
||||||
|
- id: victron_mqtt_sync_target_mode
|
||||||
|
alias: Victron MQTT Sync Mode Target
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: select.victron_remote_panel_mode
|
||||||
|
- platform: homeassistant
|
||||||
|
event: start
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: >-
|
||||||
|
{{ states('select.victron_remote_panel_mode') in ['charger_only', 'inverter_only', 'on', 'off'] }}
|
||||||
|
action:
|
||||||
|
- service: input_select.select_option
|
||||||
|
target:
|
||||||
|
entity_id: input_select.victron_remote_panel_mode_target
|
||||||
|
data:
|
||||||
|
option: "{{ states('select.victron_remote_panel_mode') }}"
|
||||||
|
|
||||||
|
- id: victron_mqtt_sync_target_current_limit
|
||||||
|
alias: Victron MQTT Sync Current Limit Target
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: number.victron_remote_panel_current_limit
|
||||||
|
- platform: homeassistant
|
||||||
|
event: start
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: >-
|
||||||
|
{{ states('number.victron_remote_panel_current_limit') not in ['unknown', 'unavailable'] }}
|
||||||
|
action:
|
||||||
|
- service: input_number.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_number.victron_remote_panel_current_limit_target
|
||||||
|
data:
|
||||||
|
value: "{{ states('number.victron_remote_panel_current_limit') | float(0) }}"
|
||||||
|
|
||||||
|
- id: victron_mqtt_sync_target_standby
|
||||||
|
alias: Victron MQTT Sync Standby Target
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: switch.victron_remote_panel_standby
|
||||||
|
- platform: homeassistant
|
||||||
|
event: start
|
||||||
|
action:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: state
|
||||||
|
entity_id: switch.victron_remote_panel_standby
|
||||||
|
state: "on"
|
||||||
|
sequence:
|
||||||
|
- service: input_boolean.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: input_boolean.victron_remote_panel_standby_target
|
||||||
|
- conditions:
|
||||||
|
- condition: state
|
||||||
|
entity_id: switch.victron_remote_panel_standby
|
||||||
|
state: "off"
|
||||||
|
sequence:
|
||||||
|
- service: input_boolean.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: input_boolean.victron_remote_panel_standby_target
|
||||||
Reference in New Issue
Block a user