implement some features of Venus OS
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-19 15:37:41 +11:00
parent d72e88ab7b
commit e8153e2953
21 changed files with 4143 additions and 90 deletions

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strings"
"time"
"github.com/jessevdk/go-flags"
)
@@ -11,7 +12,13 @@ import (
type config struct {
Address string `long:"address" env:"ADDRESS" default:":8080" description:"The IP/DNS and port of the machine that the application is running on."`
ReadOnly bool `long:"read_only" env:"READ_ONLY" description:"Disable all write operations and run in monitoring-only mode."`
Data struct {
Control struct {
Profile string `long:"control.profile" env:"CONTROL_PROFILE" default:"normal" description:"Write policy profile: normal, maintenance, or read_only."`
MaxCurrentLimit float64 `long:"control.max_current_limit" env:"CONTROL_MAX_CURRENT_LIMIT" default:"0" description:"Optional max AC current limit guardrail in amps (0 disables)."`
ModeChangeMinInterval time.Duration `long:"control.mode_change_min_interval" env:"CONTROL_MODE_CHANGE_MIN_INTERVAL" default:"3s" description:"Minimum time between mode changes."`
LockoutWindow time.Duration `long:"control.lockout_window" env:"CONTROL_LOCKOUT_WINDOW" default:"0s" description:"Post-command lockout window for command arbitration."`
}
Data struct {
Source string `long:"data.source" env:"DATA_SOURCE" default:"serial" description:"Set the source of data for the inverter gui. \"serial\", \"tcp\" or \"mock\""`
Host string `long:"data.host" env:"DATA_HOST" default:"localhost:8139" description:"Host to connect when source is set to tcp."`
Device string `long:"data.device" env:"DATA_DEVICE" default:"/dev/ttyUSB0" description:"TTY device to use when source is set to serial."`
@@ -26,12 +33,25 @@ type config struct {
Topic string `long:"mqtt.topic" env:"MQTT_TOPIC" default:"invertergui/updates" description:"Set the MQTT topic updates published to."`
CommandTopic string `long:"mqtt.command_topic" env:"MQTT_COMMAND_TOPIC" default:"invertergui/settings/set" description:"Set the MQTT topic that receives write commands for Victron settings/RAM variables."`
StatusTopic string `long:"mqtt.status_topic" env:"MQTT_STATUS_TOPIC" default:"invertergui/settings/status" description:"Set the MQTT topic where write command status updates are published."`
DeviceID string `long:"mqtt.device_id" env:"MQTT_DEVICE_ID" default:"invertergui" description:"Set the logical device ID used for per-device orchestration topics."`
HistorySize int `long:"mqtt.history_size" env:"MQTT_HISTORY_SIZE" default:"120" description:"Number of telemetry samples retained for rolling history summaries."`
InstanceID int `long:"mqtt.instance_id" env:"MQTT_INSTANCE_ID" default:"0" description:"Device instance ID for multi-device orchestration and Venus compatibility."`
Phase string `long:"mqtt.phase" env:"MQTT_PHASE" default:"L1" description:"Electrical phase label for this instance (L1/L2/L3)."`
PhaseGroup string `long:"mqtt.phase_group" env:"MQTT_PHASE_GROUP" default:"default" description:"Grouping key for parallel/3-phase system aggregation topics."`
HA struct {
Enabled bool `long:"mqtt.ha.enabled" env:"MQTT_HA_ENABLED" description:"Enable Home Assistant MQTT discovery integration."`
DiscoveryPrefix string `long:"mqtt.ha.discovery_prefix" env:"MQTT_HA_DISCOVERY_PREFIX" default:"homeassistant" description:"Set Home Assistant MQTT discovery prefix."`
NodeID string `long:"mqtt.ha.node_id" env:"MQTT_HA_NODE_ID" default:"invertergui" description:"Set Home Assistant node ID used for discovery topics and unique IDs."`
DeviceName string `long:"mqtt.ha.device_name" env:"MQTT_HA_DEVICE_NAME" default:"Victron Inverter" description:"Set Home Assistant device display name."`
}
Venus struct {
Enabled bool `long:"mqtt.venus.enabled" env:"MQTT_VENUS_ENABLED" description:"Enable Venus-style MQTT compatibility topics (N/W topic model)."`
PortalID string `long:"mqtt.venus.portal_id" env:"MQTT_VENUS_PORTAL_ID" default:"invertergui" description:"Set Venus portal ID segment used in N/W topics."`
Service string `long:"mqtt.venus.service" env:"MQTT_VENUS_SERVICE" default:"vebus/257" description:"Set Venus service segment used in N/W topics."`
SubscribeWrites bool `long:"mqtt.venus.subscribe_writes" env:"MQTT_VENUS_SUBSCRIBE_WRITES" default:"true" description:"Subscribe to Venus write topics and map them to MK2 control commands."`
TopicPrefix string `long:"mqtt.venus.topic_prefix" env:"MQTT_VENUS_TOPIC_PREFIX" default:"" description:"Optional topic prefix before Venus N/W topics, for example 'victron'."`
GuideCompat bool `long:"mqtt.venus.guide_compat" env:"MQTT_VENUS_GUIDE_COMPAT" default:"true" description:"Enable guide-style settings/0/Settings/CGwacs compatibility paths for Home Assistant controls."`
}
Username string `long:"mqtt.username" env:"MQTT_USERNAME" default:"" description:"Set the MQTT username"`
Password string `long:"mqtt.password" env:"MQTT_PASSWORD" default:"" description:"Set the MQTT password"`
PasswordFile string `long:"mqtt.password-file" env:"MQTT_PASSWORD_FILE" default:"" description:"Path to a file containing the MQTT password"`

View File

@@ -36,6 +36,7 @@ import (
"net"
"net/http"
"os"
"strings"
"git.coadcorp.com/nathan/invertergui/mk2core"
"git.coadcorp.com/nathan/invertergui/mk2driver"
@@ -77,7 +78,15 @@ func main() {
"mqtt_topic": conf.MQTT.Topic,
"mqtt_command_topic": conf.MQTT.CommandTopic,
"mqtt_status_topic": conf.MQTT.StatusTopic,
"mqtt_device_id": conf.MQTT.DeviceID,
"mqtt_history_size": conf.MQTT.HistorySize,
"mqtt_ha_enabled": conf.MQTT.HA.Enabled,
"mqtt_venus_enabled": conf.MQTT.Venus.Enabled,
"mqtt_venus_portal": conf.MQTT.Venus.PortalID,
"mqtt_venus_service": conf.MQTT.Venus.Service,
"mqtt_venus_prefix": conf.MQTT.Venus.TopicPrefix,
"mqtt_venus_guide": conf.MQTT.Venus.GuideCompat,
"control_profile": conf.Control.Profile,
}).Info("Configuration loaded")
mk2, err := getMk2Device(conf.Data.Source, conf.Data.Host, conf.Data.Device)
@@ -109,6 +118,36 @@ func main() {
log.Info("READ_ONLY enabled")
}
writer = nil
} else if writer != nil {
policyProfile := mk2driver.WriterProfile(strings.ToLower(strings.TrimSpace(conf.Control.Profile)))
if policyProfile == "" {
policyProfile = mk2driver.WriterProfileNormal
}
if policyProfile != mk2driver.WriterProfileNormal &&
policyProfile != mk2driver.WriterProfileMaintenance &&
policyProfile != mk2driver.WriterProfileReadOnly {
log.WithField("profile", conf.Control.Profile).Warn("Unknown control profile; defaulting to normal")
policyProfile = mk2driver.WriterProfileNormal
}
var maxCurrentLimit *float64
if conf.Control.MaxCurrentLimit > 0 {
limit := conf.Control.MaxCurrentLimit
maxCurrentLimit = &limit
}
writer = mk2driver.NewManagedWriter(writer, mk2driver.WriterPolicy{
Profile: policyProfile,
MaxCurrentLimitA: maxCurrentLimit,
ModeChangeMinInterval: conf.Control.ModeChangeMinInterval,
LockoutWindow: conf.Control.LockoutWindow,
})
log.WithFields(logrus.Fields{
"profile": policyProfile,
"max_current_limit": conf.Control.MaxCurrentLimit,
"mode_change_min_interval": conf.Control.ModeChangeMinInterval,
"lockout_window": conf.Control.LockoutWindow,
}).Info("Write policy/arbitration layer enabled")
}
gui := webui.NewWebGui(core.NewSubscription(), writer)
http.Handle("/", static.New())
@@ -136,12 +175,25 @@ func main() {
CommandTopic: conf.MQTT.CommandTopic,
StatusTopic: conf.MQTT.StatusTopic,
ClientID: conf.MQTT.ClientID,
DeviceID: conf.MQTT.DeviceID,
HistorySize: conf.MQTT.HistorySize,
InstanceID: conf.MQTT.InstanceID,
Phase: conf.MQTT.Phase,
PhaseGroup: conf.MQTT.PhaseGroup,
HomeAssistant: mqttclient.HomeAssistantConfig{
Enabled: conf.MQTT.HA.Enabled,
DiscoveryPrefix: conf.MQTT.HA.DiscoveryPrefix,
NodeID: conf.MQTT.HA.NodeID,
DeviceName: conf.MQTT.HA.DeviceName,
},
Venus: mqttclient.VenusConfig{
Enabled: conf.MQTT.Venus.Enabled,
PortalID: conf.MQTT.Venus.PortalID,
Service: conf.MQTT.Venus.Service,
SubscribeWrites: conf.MQTT.Venus.SubscribeWrites,
TopicPrefix: conf.MQTT.Venus.TopicPrefix,
GuideCompat: conf.MQTT.Venus.GuideCompat,
},
Username: conf.MQTT.Username,
Password: conf.MQTT.Password,
}