From 03f45a849d1dfab48e935a54e664743efae1b60e Mon Sep 17 00:00:00 2001
From: jagheterfredrik <jagheterfredrik@gmail.com>
Date: Sat, 23 Dec 2023 21:34:37 +0100
Subject: [PATCH] move to go version

---
 .github/workflows/go.yml        |  25 ++
 .gitignore                      | 166 +------------
 .pre-commit-config.yaml         |  42 ----
 README.md                       |  21 +-
 app/bridge.go                   | 127 ++++++++++
 app/config.go                   |  37 +++
 app/ratelimit/rate_limiter.go   |  35 +++
 app/sensors.go                  | 179 ++++++++++++++
 app/tui.go                      |  92 ++++++++
 app/wallbox/mq_darwin.go        |   5 +
 app/wallbox/mq_linux.go         |  39 ++++
 app/wallbox/mq_linux_amd64.go   |   6 +
 app/wallbox/mq_linux_arm.go     |   6 +
 app/wallbox/mq_linux_arm64.go   |   6 +
 app/wallbox/wallbox.go          | 199 ++++++++++++++++
 app/wallbox/wallbox_const.go    |  91 ++++++++
 go.mod                          |  19 ++
 go.sum                          |  23 ++
 main.go                         |  19 ++
 make.sh                         |   4 +
 mqtt-bridge/bridge.ini          |  10 -
 mqtt-bridge/bridge.py           | 399 --------------------------------
 mqtt-bridge/install.sh          |  40 ----
 mqtt-bridge/mqtt-bridge.service |  15 --
 mqtt-bridge/requirements.txt    |   3 -
 pyproject.toml                  |   2 -
 26 files changed, 923 insertions(+), 687 deletions(-)
 create mode 100644 .github/workflows/go.yml
 delete mode 100644 .pre-commit-config.yaml
 create mode 100644 app/bridge.go
 create mode 100644 app/config.go
 create mode 100644 app/ratelimit/rate_limiter.go
 create mode 100644 app/sensors.go
 create mode 100644 app/tui.go
 create mode 100644 app/wallbox/mq_darwin.go
 create mode 100644 app/wallbox/mq_linux.go
 create mode 100644 app/wallbox/mq_linux_amd64.go
 create mode 100644 app/wallbox/mq_linux_arm.go
 create mode 100644 app/wallbox/mq_linux_arm64.go
 create mode 100644 app/wallbox/wallbox.go
 create mode 100644 app/wallbox/wallbox_const.go
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100644 main.go
 create mode 100755 make.sh
 delete mode 100644 mqtt-bridge/bridge.ini
 delete mode 100644 mqtt-bridge/bridge.py
 delete mode 100755 mqtt-bridge/install.sh
 delete mode 100644 mqtt-bridge/mqtt-bridge.service
 delete mode 100644 mqtt-bridge/requirements.txt
 delete mode 100644 pyproject.toml

diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..a004b8f
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,25 @@
+# This workflow will build a golang project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
+
+name: Go
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    branches: [ "main" ]
+
+jobs:
+
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+
+    - name: Set up Go
+      uses: actions/setup-go@v4
+      with:
+        go-version: '1.20'
+
+    - name: Build
+      run: ./make.sh
diff --git a/.gitignore b/.gitignore
index 68bc17f..8d36e43 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,160 +1,12 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
+# Test binary, built with `go test -c`
+*.test
 
-# C extensions
-*.so
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
 
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
+# make.sh output
+bridge-armhf
+bridge-arm64
 
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-#   For a library or package, you might want to ignore these files since the code is
-#   intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-#   However, in case of collaboration, if having platform-specific dependencies or dependencies
-#   having no cross-platform support, pipenv may install dependencies that don't work, or not
-#   install all needed dependencies.
-#Pipfile.lock
-
-# poetry
-#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-#   This is especially recommended for binary packages to ensure reproducibility, and is more
-#   commonly ignored for libraries.
-#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-#   in version control.
-#   https://pdm.fming.dev/#use-with-ide
-.pdm.toml
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-#  and can be added to the global gitignore or merged into this file.  For a more nuclear
-#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+# Go workspace file
+go.work
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
deleted file mode 100644
index 7e48b17..0000000
--- a/.pre-commit-config.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
----
-# Some expensive checks are disabled by default. To run all:
-# pre-commit run --all-files --hook-stage manual
-repos:
-  - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
-    hooks:
-      - id: check-added-large-files
-      - id: check-case-conflict
-      - id: check-executables-have-shebangs
-      - id: check-merge-conflict
-      - id: check-shebang-scripts-are-executable
-      - id: check-vcs-permalinks
-      - id: debug-statements
-      - id: detect-private-key
-      - id: end-of-file-fixer
-      - id: fix-byte-order-marker
-      - id: mixed-line-ending
-      - id: trailing-whitespace
-  - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.1.8
-    hooks:
-      - id: ruff
-        args: [--fix]
-      - id: ruff-format
-  - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.7.1
-    hooks:
-      - id: mypy
-        additional_dependencies: [paho-mqtt, pymysql, redis, types-PyMySQL, types-paho-mqtt]
-  - repo: https://github.com/codespell-project/codespell
-    rev: v2.2.6
-    hooks:
-      - id: codespell
-  - repo: https://github.com/koalaman/shellcheck-precommit
-    rev: v0.9.0
-    hooks:
-      - id: shellcheck
-        stages: [manual]
-
-ci:
-  autoupdate_schedule: monthly
diff --git a/README.md b/README.md
index 85356fd..62f6b92 100644
--- a/README.md
+++ b/README.md
@@ -21,27 +21,10 @@ This open-source project connects your Wallbox fully locally to Home Assistant,
 
 1. [Root your Wallbox](https://github.com/jagheterfredrik/wallbox-pwn)
 2. Setup an MQTT Broker, if you don't already have one. Here's an example [installing it as a Home Assistant add-on](https://www.youtube.com/watch?v=dqTn-Gk4Qeo)
-3. Edit bridge.ini
-   - Set `host` the IP address of your MQTT broker
-   - Set `username` and `password` to match your broker setup
-4. Copy the files in mqtt-bridge to your Wallbox.
-
-   On Windows you can use WinSCP.
-
-   On OS X/Linux this can be done using `scp -r /path/to/wallbox-mqtt-bridge/mqtt-bridge root@<wallbox-ip>:~`
-
-   You should end up with the following files in your Wallbox:
-      - `/home/root/mqtt-bridge/bridge.ini`
-      - `/home/root/mqtt-bridge/bridge.py`
-      - `/home/root/mqtt-bridge/install.sh`
-      - `/home/root/mqtt-bridge/mqtt-bridge.service`
-      - `/home/root/mqtt-bridge/requirements.txt`
-5. `ssh` to your Wallbox and run the installer
+3. `ssh` to your Wallbox and run
 
 ```sh
-cd mqtt-bridge
-chmod +x install.sh
-./install.sh
+curl -sSfL https://github.com/jagheterfredrik/wallbox-mqtt-bridge/releases/download/bridge/install.sh > install.sh && bash install.sh
 ```
 
 ## Acknowledgments
diff --git a/app/bridge.go b/app/bridge.go
new file mode 100644
index 0000000..60742e1
--- /dev/null
+++ b/app/bridge.go
@@ -0,0 +1,127 @@
+package bridge
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/signal"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/eclipse/paho.mqtt.golang"
+	"github.com/jagheterfredrik/wallbox-mqtt-bridge/app/ratelimit"
+	"github.com/jagheterfredrik/wallbox-mqtt-bridge/app/wallbox"
+)
+
+var connectLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {
+	panic("Connection to MQTT lost")
+}
+
+func LaunchBridge(configPath string) {
+	c := LoadConfig(configPath)
+	w := wallbox.New()
+	w.RefreshData()
+
+	serialNumber := w.SerialNumber()
+	entityConfig := getEntities(w)
+	if c.Settings.DebugSensors {
+		for k, v := range getDebugEntities(w) {
+			entityConfig[k] = v
+		}
+	}
+
+	topicPrefix := "wallbox_" + serialNumber
+	availabilityTopic := topicPrefix + "/availability"
+
+	opts := mqtt.NewClientOptions()
+	opts.AddBroker(fmt.Sprintf("tcp://%s:%d", c.MQTT.Host, c.MQTT.Port))
+	opts.SetUsername(c.MQTT.Username)
+	opts.SetPassword(c.MQTT.Password)
+	opts.SetWill(availabilityTopic, "offline", 1, true)
+	opts.OnConnectionLost = connectLostHandler
+
+	client := mqtt.NewClient(opts)
+	if token := client.Connect(); token.Wait() && token.Error() != nil {
+		panic(token.Error())
+	}
+
+	for key, val := range entityConfig {
+		component := val.Component
+		uid := serialNumber + "_" + key
+		config := map[string]interface{}{
+			"~":                  topicPrefix + "/" + key,
+			"availability_topic": availabilityTopic,
+			"state_topic":        "~/state",
+			"unique_id":          uid,
+			"device": map[string]string{
+				"identifiers": serialNumber,
+				"name":        c.Settings.DeviceName,
+			},
+		}
+		if val.Setter != nil {
+			config["command_topic"] = "~/set"
+		}
+		for k, v := range val.Config {
+			config[k] = v
+		}
+		jsonPayload, _ := json.Marshal(config)
+		token := client.Publish("homeassistant/"+component+"/"+uid+"/config", 1, true, jsonPayload)
+		token.Wait()
+	}
+
+	token := client.Publish(availabilityTopic, 1, true, "online")
+	token.Wait()
+
+	messageHandler := func(client mqtt.Client, msg mqtt.Message) {
+		field := strings.Split(msg.Topic(), "/")[1]
+		payload := string(msg.Payload())
+		setter := entityConfig[field].Setter
+		fmt.Println("Setting", field, payload)
+		setter(payload)
+	}
+
+	topic := topicPrefix + "/+/set"
+	client.Subscribe(topic, 1, messageHandler)
+
+	ticker := time.NewTicker(time.Duration(c.Settings.PollingIntervalSeconds) * time.Second)
+	defer ticker.Stop()
+
+	published := make(map[string]interface{})
+	rateLimiter := map[string]*ratelimit.DeltaRateLimit{
+		"charging_power": ratelimit.NewDeltaRateLimit(10, 100),
+		"added_energy":   ratelimit.NewDeltaRateLimit(10, 50),
+	}
+
+	for {
+		select {
+		case <-ticker.C:
+			w.RefreshData()
+			for key, val := range entityConfig {
+				payload := val.Getter()
+				bytePayload := []byte(fmt.Sprint(payload))
+				if published[key] != payload {
+					if rate, ok := rateLimiter[key]; ok && !rate.Allow(strToFloat(payload)) {
+						continue
+					}
+					fmt.Println("Publishing: ", key, payload)
+					token := client.Publish(topicPrefix+"/"+key+"/state", 1, true, bytePayload)
+					token.Wait()
+					published[key] = payload
+				}
+			}
+		case <-interrupt():
+			fmt.Println("Interrupted. Exiting...")
+			token := client.Publish(availabilityTopic, 1, true, "offline")
+			token.Wait()
+			client.Disconnect(250)
+			os.Exit(0)
+		}
+	}
+}
+
+func interrupt() <-chan os.Signal {
+	interrupt := make(chan os.Signal, 1)
+	signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
+	return interrupt
+}
diff --git a/app/config.go b/app/config.go
new file mode 100644
index 0000000..2b2f1f0
--- /dev/null
+++ b/app/config.go
@@ -0,0 +1,37 @@
+package bridge
+
+import (
+	"gopkg.in/ini.v1"
+)
+
+type WallboxConfig struct {
+	MQTT struct {
+		Host     string `ini:"host"`
+		Port     int    `ini:"port"`
+		Username string `ini:"username"`
+		Password string `ini:"password"`
+	} `ini:"mqtt"`
+
+	Settings struct {
+		PollingIntervalSeconds int    `ini:"polling_interval_seconds"`
+		DeviceName             string `ini:"device_name"`
+		DebugSensors           bool   `ini:"debug_sensors"`
+	} `ini:"settings"`
+}
+
+func (w *WallboxConfig) SaveTo(path string) {
+	cfg := ini.Empty()
+	cfg.ReflectFrom(w)
+	cfg.SaveTo(path)
+}
+
+func LoadConfig(path string) *WallboxConfig {
+	cfg, _ := ini.Load(path)
+
+	var config WallboxConfig
+	if err := cfg.MapTo(&config); err != nil {
+		return nil
+	}
+
+	return &config
+}
diff --git a/app/ratelimit/rate_limiter.go b/app/ratelimit/rate_limiter.go
new file mode 100644
index 0000000..61a938f
--- /dev/null
+++ b/app/ratelimit/rate_limiter.go
@@ -0,0 +1,35 @@
+package ratelimit
+
+import (
+	"math"
+	"time"
+)
+
+type DeltaRateLimit struct {
+	lastTime    time.Time
+	lastValue   float64
+	interval    time.Duration
+	valueChange float64
+}
+
+func NewDeltaRateLimit(interval time.Duration, valueChange float64) *DeltaRateLimit {
+	return &DeltaRateLimit{
+		interval:    interval * time.Second,
+		valueChange: valueChange,
+	}
+}
+
+func (c *DeltaRateLimit) Allow(value float64) bool {
+	now := time.Now()
+
+	if math.Abs(value-c.lastValue) < c.valueChange {
+		if now.Sub(c.lastTime) < c.interval {
+			return false
+		}
+	}
+
+	c.lastTime = now
+	c.lastValue = value
+
+	return true
+}
diff --git a/app/sensors.go b/app/sensors.go
new file mode 100644
index 0000000..1241e9b
--- /dev/null
+++ b/app/sensors.go
@@ -0,0 +1,179 @@
+package bridge
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/jagheterfredrik/wallbox-mqtt-bridge/app/wallbox"
+)
+
+type Entity struct {
+	Component string
+	Getter    func() string
+	Setter    func(string)
+	Config    map[string]string
+}
+
+func strToInt(val string) int {
+	i, _ := strconv.Atoi(val)
+	return i
+}
+
+func strToFloat(val string) float64 {
+	f, _ := strconv.ParseFloat(val, 64)
+	return f
+}
+
+func getEntities(w *wallbox.Wallbox) map[string]Entity {
+	return map[string]Entity{
+		"added_energy": {
+			Component: "sensor",
+			Getter:    func() string { return fmt.Sprint(w.Data.RedisState.ScheduleEnergy) },
+			Config: map[string]string{
+				"name":                        "Added energy",
+				"device_class":                "energy",
+				"unit_of_measurement":         "Wh",
+				"state_class":                 "total",
+				"suggested_display_precision": "1",
+			},
+		},
+		"added_range": {
+			Component: "sensor",
+			Getter:    func() string { return fmt.Sprint(w.Data.SQL.AddedRange) },
+			Config: map[string]string{
+				"name":                        "Added range",
+				"device_class":                "distance",
+				"unit_of_measurement":         "km",
+				"state_class":                 "total",
+				"suggested_display_precision": "1",
+				"icon":                        "mdi:map-marker-distance",
+			},
+		},
+		"cable_connected": {
+			Component: "binary_sensor",
+			Getter:    func() string { return strconv.Itoa(w.CableConnected()) },
+			Config: map[string]string{
+				"name":         "Cable connected",
+				"payload_on":   "1",
+				"payload_off":  "0",
+				"icon":         "mdi:ev-plug-type1",
+				"device_class": "plug",
+			},
+		},
+		"charging_enable": {
+			Component: "switch",
+			Setter:    func(val string) { w.SetChargingEnable(strToInt(val)) },
+			Getter:    func() string { return strconv.Itoa(w.Data.SQL.ChargingEnable) },
+			Config: map[string]string{
+				"name":        "Charging enable",
+				"payload_on":  "1",
+				"payload_off": "0",
+				"icon":        "mdi:ev-station",
+			},
+		},
+		"charging_power": {
+			Component: "sensor",
+			Getter: func() string {
+				return fmt.Sprint(w.Data.RedisM2W.Line1Power + w.Data.RedisM2W.Line2Power + w.Data.RedisM2W.Line3Power)
+			},
+			Config: map[string]string{
+				"name":                        "Charging power",
+				"device_class":                "power",
+				"unit_of_measurement":         "W",
+				"state_class":                 "total",
+				"suggested_display_precision": "1",
+			},
+		},
+		"cumulative_added_energy": {
+			Component: "sensor",
+			Getter:    func() string { return fmt.Sprint(w.Data.SQL.CumulativeAddedEnergy) },
+			Config: map[string]string{
+				"name":                        "Cumulative added energy",
+				"device_class":                "energy",
+				"unit_of_measurement":         "Wh",
+				"state_class":                 "total_increasing",
+				"suggested_display_precision": "1",
+			},
+		},
+		"halo_brightness": {
+			Component: "number",
+			Setter:    func(val string) { w.SetHaloBrightness(strToInt(val)) },
+			Getter:    func() string { return strconv.Itoa(w.Data.SQL.HaloBrightness) },
+			Config: map[string]string{
+				"name":                "Halo Brightness",
+				"command_topic":       "~/set",
+				"min":                 "0",
+				"max":                 "100",
+				"icon":                "mdi:brightness-percent",
+				"unit_of_measurement": "%",
+				"entity_category":     "config",
+			},
+		},
+		"lock": {
+			Component: "lock",
+			Setter:    func(val string) { w.SetLocked(strToInt(val)) },
+			Getter:    func() string { return strconv.Itoa(w.Data.SQL.Lock) },
+			Config: map[string]string{
+				"name":           "Lock",
+				"payload_lock":   "1",
+				"payload_unlock": "0",
+				"state_locked":   "1",
+				"state_unlocked": "0",
+				"command_topic":  "~/set",
+			},
+		},
+		"max_charging_current": {
+			Component: "number",
+			Setter:    func(val string) { w.SetMaxChargingCurrent(strToInt(val)) },
+			Getter:    func() string { return strconv.Itoa(w.Data.SQL.MaxChargingCurrent) },
+			Config: map[string]string{
+				"name":                "Max charging current",
+				"command_topic":       "~/set",
+				"min":                 "6",
+				"max":                 strconv.Itoa(w.AvailableCurrent()),
+				"unit_of_measurement": "A",
+				"device_class":        "current",
+			},
+		},
+		"status": {
+			Component: "sensor",
+			Getter:    w.EffectiveStatus,
+			Config: map[string]string{
+				"name": "Status",
+			},
+		},
+	}
+}
+
+func getDebugEntities(w *wallbox.Wallbox) map[string]Entity {
+	return map[string]Entity{
+		"control_pilot": {
+			Component: "sensor",
+			Getter:    w.ControlPilotStatus,
+			Config: map[string]string{
+				"name": "Control pilot",
+			},
+		},
+		"m2w_status": {
+			Component: "sensor",
+			Getter:    func() string { return fmt.Sprint(w.Data.RedisM2W.ChargerStatus) },
+			Config: map[string]string{
+				"name": "M2W Status",
+			},
+		},
+		"state_machine_state": {
+			Component: "sensor",
+			Getter:    w.StateMachineState,
+			Config: map[string]string{
+				"name": "State machine",
+			},
+		},
+		"s2_open": {
+			Component: "sensor",
+			Getter:    func() string { return strconv.Itoa(w.Data.RedisState.S2open) },
+			Config: map[string]string{
+				"name": "S2 open",
+			},
+		},
+	}
+}
diff --git a/app/tui.go b/app/tui.go
new file mode 100644
index 0000000..4723d45
--- /dev/null
+++ b/app/tui.go
@@ -0,0 +1,92 @@
+package bridge
+
+import (
+	"bufio"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+)
+
+var service = `[Unit]
+Description=MQTT Bridge
+After=network.target
+Requires=mysqld.service
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=root
+ExecStart=/home/root/mqtt-bridge/bridge /home/root/mqtt-bridge/bridge.ini
+
+[Install]
+WantedBy=multi-user.target
+`
+
+func askConfirmOrNew(field *string, name string) {
+	fmt.Printf("%s (%s): ", name, *field)
+	reader := bufio.NewReader(os.Stdin)
+	input, _ := reader.ReadString('\n')
+	input = strings.TrimSpace(input)
+	if len(input) > 0 {
+		*field = input
+	}
+}
+
+func askConfirmOrNewInt(field *int, name string) {
+	fmt.Printf("%s (%d): ", name, *field)
+	reader := bufio.NewReader(os.Stdin)
+	input, _ := reader.ReadString('\n')
+	input = strings.TrimSpace(input)
+	if len(input) > 0 {
+		*field, _ = strconv.Atoi(input)
+	}
+}
+
+func askConfirmOrNewBool(field *bool, name string) {
+	fmt.Printf("%s (y/N): ", name)
+	reader := bufio.NewReader(os.Stdin)
+	input, _ := reader.ReadString('\n')
+	input = strings.TrimSpace(input)
+	if len(input) > 0 && input == "y" {
+		*field = true
+	}
+}
+
+func installService() {
+	ioutil.WriteFile("/lib/systemd/system/mqtt-bridge.service", []byte(service), 0644)
+	var cmd *exec.Cmd
+	cmd = exec.Command("systemctl", "daemon-reload")
+	cmd.Run()
+	cmd = exec.Command("systemctl", "enable", "mqtt-bridge")
+	cmd.Run()
+	cmd = exec.Command("systemctl", "restart", "mqtt-bridge")
+	cmd.Run()
+}
+
+func RunTuiSetup() {
+	config := WallboxConfig{}
+	config.MQTT.Host = "127.0.0.1"
+	config.MQTT.Port = 1883
+	config.MQTT.Username = ""
+	config.MQTT.Password = ""
+	config.Settings.PollingIntervalSeconds = 1
+	config.Settings.DeviceName = "Wallbox"
+	config.Settings.DebugSensors = false
+
+	askConfirmOrNew(&config.MQTT.Host, "MQTT Host")
+	askConfirmOrNewInt(&config.MQTT.Port, "MQTT Port")
+	askConfirmOrNew(&config.MQTT.Username, "MQTT Username")
+	askConfirmOrNew(&config.MQTT.Password, "MQTT Password")
+	askConfirmOrNewInt(&config.Settings.PollingIntervalSeconds, "Polling interval")
+	askConfirmOrNew(&config.Settings.DeviceName, "Device name")
+	askConfirmOrNewBool(&config.Settings.DebugSensors, "Debug sensors")
+
+	config.SaveTo("bridge.ini")
+
+	installService()
+}
diff --git a/app/wallbox/mq_darwin.go b/app/wallbox/mq_darwin.go
new file mode 100644
index 0000000..76867ce
--- /dev/null
+++ b/app/wallbox/mq_darwin.go
@@ -0,0 +1,5 @@
+package wallbox
+
+func mqOpen(path []byte) uintptr                  { return 0 }
+func mqTimedsend(fd uintptr, data []byte) uintptr { return 0 }
+func mqClose(fd uintptr)                          {}
diff --git a/app/wallbox/mq_linux.go b/app/wallbox/mq_linux.go
new file mode 100644
index 0000000..cc87110
--- /dev/null
+++ b/app/wallbox/mq_linux.go
@@ -0,0 +1,39 @@
+package wallbox
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+func mqOpen(path []byte) uintptr {
+	mq, _, _ := syscall.Syscall6(
+		uintptr(MqOpenSyscall),
+		uintptr(unsafe.Pointer(&path[0])),
+		uintptr(0x02),
+		uintptr(0x1c7),
+		uintptr(0),
+		uintptr(0),
+		uintptr(0),
+	)
+
+	return mq
+}
+
+func mqTimedsend(fd uintptr, data []byte) uintptr {
+	mqLock, _, _ := syscall.Syscall6(
+		uintptr(MqTimedSendSyscall),
+		uintptr(fd),
+		uintptr(unsafe.Pointer(&data[0])),
+		uintptr(uintptr(len(data))),
+		uintptr(0),
+		uintptr(0),
+		uintptr(0),
+	)
+
+	return mqLock
+}
+
+func mqClose(fd uintptr) {
+	fdi := int(fd)
+	syscall.Close(fdi)
+}
diff --git a/app/wallbox/mq_linux_amd64.go b/app/wallbox/mq_linux_amd64.go
new file mode 100644
index 0000000..cf9717b
--- /dev/null
+++ b/app/wallbox/mq_linux_amd64.go
@@ -0,0 +1,6 @@
+package wallbox
+
+const (
+	MqOpenSyscall      = 240
+	MqTimedSendSyscall = 242
+)
diff --git a/app/wallbox/mq_linux_arm.go b/app/wallbox/mq_linux_arm.go
new file mode 100644
index 0000000..2893919
--- /dev/null
+++ b/app/wallbox/mq_linux_arm.go
@@ -0,0 +1,6 @@
+package wallbox
+
+const (
+	MqOpenSyscall      = 274
+	MqTimedSendSyscall = 276
+)
diff --git a/app/wallbox/mq_linux_arm64.go b/app/wallbox/mq_linux_arm64.go
new file mode 100644
index 0000000..05d8fff
--- /dev/null
+++ b/app/wallbox/mq_linux_arm64.go
@@ -0,0 +1,6 @@
+package wallbox
+
+const (
+	MqOpenSyscall      = 180
+	MqTimedSendSyscall = 182
+)
diff --git a/app/wallbox/wallbox.go b/app/wallbox/wallbox.go
new file mode 100644
index 0000000..6cd04d3
--- /dev/null
+++ b/app/wallbox/wallbox.go
@@ -0,0 +1,199 @@
+package wallbox
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"reflect"
+
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/jmoiron/sqlx"
+	"github.com/redis/go-redis/v9"
+)
+
+type DataCache struct {
+	SQL struct {
+		Lock                  int     `db:"lock"`
+		ChargingEnable        int     `db:"charging_enable"`
+		MaxChargingCurrent    int     `db:"max_charging_current"`
+		HaloBrightness        int     `db:"halo_brightness"`
+		CumulativeAddedEnergy float64 `db:"cumulative_added_energy"`
+		AddedRange            float64 `db:"added_range"`
+	}
+
+	RedisState struct {
+		SessionState   int     `redis:"session.state"`
+		ControlPilot   int     `redis:"ctrlPilot"`
+		S2open         int     `redis:"S2open"`
+		ScheduleEnergy float64 `redis:"scheduleEnergy"`
+	}
+
+	RedisM2W struct {
+		ChargerStatus int     `redis:"tms.charger_status"`
+		Line1Power    float64 `redis:"tms.line1.power_watt.value"`
+		Line2Power    float64 `redis:"tms.line2.power_watt.value"`
+		Line3Power    float64 `redis:"tms.line3.power_watt.value"`
+	}
+}
+
+type Wallbox struct {
+	redisClient *redis.Client
+	sqlClient   *sqlx.DB
+	Data        DataCache
+}
+
+func New() *Wallbox {
+	var w Wallbox
+
+	var err error
+	w.sqlClient, err = sqlx.Connect("mysql", "root:fJmExsJgmKV7cq8H@tcp(127.0.0.1:3306)/wallbox")
+	if err != nil {
+		panic(err)
+	}
+
+	w.redisClient = redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "",
+		DB:       0,
+	})
+
+	return &w
+}
+
+func getRedisFields(obj interface{}) []string {
+	var result []string
+	val := reflect.ValueOf(obj)
+	typ := val.Type()
+
+	for i := 0; i < val.NumField(); i++ {
+		field := typ.Field(i)
+		result = append(result, field.Tag.Get("redis"))
+	}
+
+	return result
+}
+
+func (w *Wallbox) RefreshData() {
+	ctx := context.Background()
+
+	stateRes := w.redisClient.HMGet(ctx, "state", getRedisFields(w.Data.RedisState)...)
+	if stateRes.Err() != nil {
+		panic(stateRes.Err())
+	}
+
+	if err := stateRes.Scan(&w.Data.RedisState); err != nil {
+		panic(err)
+	}
+
+	m2wRes := w.redisClient.HMGet(ctx, "m2w", getRedisFields(w.Data.RedisM2W)...)
+	if m2wRes.Err() != nil {
+		panic(m2wRes.Err())
+	}
+
+	if err := m2wRes.Scan(&w.Data.RedisM2W); err != nil {
+		panic(err)
+	}
+
+	query := "SELECT " +
+		"  `wallbox_config`.`charging_enable`," +
+		"  `wallbox_config`.`lock`," +
+		"  `wallbox_config`.`max_charging_current`," +
+		"  `wallbox_config`.`halo_brightness`," +
+		"  `power_outage_values`.`charged_energy` AS cumulative_added_energy," +
+		"  IF(`active_session`.`unique_id` != 0," +
+		"    `active_session`.`charged_range`," +
+		"    `latest_session`.`charged_range`) AS added_range " +
+		"FROM `wallbox_config`," +
+		"    `active_session`," +
+		"    `power_outage_values`," +
+		"    (SELECT * FROM `session` ORDER BY `id` DESC LIMIT 1) AS latest_session"
+	w.sqlClient.Get(&w.Data.SQL, query)
+}
+
+func (w *Wallbox) SerialNumber() string {
+	var serialNumber string
+	w.sqlClient.Get(&serialNumber, "SELECT `serial_num` FROM charger_info")
+	return serialNumber
+}
+
+func (w *Wallbox) UserId() string {
+	var userId string
+	w.sqlClient.QueryRow("SELECT `user_id` FROM `users` WHERE `user_id` != 1 ORDER BY `user_id` DESC LIMIT 1").Scan(&userId)
+	return userId
+}
+
+func (w *Wallbox) AvailableCurrent() int {
+	var availableCurrent int
+	w.sqlClient.QueryRow("SELECT `max_avbl_current` FROM `state_values` ORDER BY `id` DESC LIMIT 1").Scan(&availableCurrent)
+	return availableCurrent
+}
+
+func sendToPosixQueue(path, data string) {
+	pathBytes := append([]byte(path), 0)
+	mq := mqOpen(pathBytes)
+
+	event := []byte(data)
+	eventPaddedBytes := append(event, bytes.Repeat([]byte{0x00}, 1024-len(event))...)
+
+	mqTimedsend(mq, eventPaddedBytes)
+	mqClose(mq)
+}
+
+func (w *Wallbox) SetLocked(lock int) {
+	w.RefreshData()
+	if lock == w.Data.SQL.Lock {
+		return
+	}
+	if lock == 1 {
+		sendToPosixQueue("WALLBOX_MYWALLBOX_WALLBOX_LOGIN", "EVENT_REQUEST_LOCK")
+	} else {
+		userId := w.UserId()
+		sendToPosixQueue("WALLBOX_MYWALLBOX_WALLBOX_LOGIN", "EVENT_REQUEST_LOGIN#"+userId+".000000")
+	}
+}
+
+func (w *Wallbox) SetChargingEnable(enable int) {
+	w.RefreshData()
+	if enable == w.Data.SQL.ChargingEnable {
+		return
+	}
+	if enable == 1 {
+		sendToPosixQueue("WALLBOX_MYWALLBOX_WALLBOX_STATEMACHINE", "EVENT_REQUEST_USER_ACTION#1.000000")
+	} else {
+		sendToPosixQueue("WALLBOX_MYWALLBOX_WALLBOX_STATEMACHINE", "EVENT_REQUEST_USER_ACTION#2.000000")
+	}
+}
+
+func (w *Wallbox) SetMaxChargingCurrent(current int) {
+	w.sqlClient.MustExec("UPDATE `wallbox_config` SET `max_charging_current`=?", current)
+}
+
+func (w *Wallbox) SetHaloBrightness(brightness int) {
+	w.sqlClient.MustExec("UPDATE `wallbox_config` SET `halo_brightness`=?", brightness)
+}
+
+func (w *Wallbox) CableConnected() int {
+	if w.Data.RedisM2W.ChargerStatus == 0 || w.Data.RedisM2W.ChargerStatus == 6 {
+		return 0
+	}
+	return 1
+}
+
+func (w *Wallbox) EffectiveStatus() string {
+	tmsStatus := w.Data.RedisM2W.ChargerStatus
+	state := w.Data.RedisState.SessionState
+
+	if override, ok := stateOverrides[state]; ok {
+		tmsStatus = override
+	}
+
+	return wallboxStatusCodes[tmsStatus]
+}
+
+func (w *Wallbox) ControlPilotStatus() string {
+	return fmt.Sprintf("%d: %s", w.Data.RedisState.ControlPilot, controlPilotStates[w.Data.RedisState.ControlPilot])
+}
+
+func (w *Wallbox) StateMachineState() string {
+	return fmt.Sprintf("%d: %s", w.Data.RedisState.SessionState, stateMachineStates[w.Data.RedisState.SessionState])
+}
diff --git a/app/wallbox/wallbox_const.go b/app/wallbox/wallbox_const.go
new file mode 100644
index 0000000..81e9524
--- /dev/null
+++ b/app/wallbox/wallbox_const.go
@@ -0,0 +1,91 @@
+package wallbox
+
+var wallboxStatusCodes = []string{
+	"Ready",
+	"Charging",
+	"Connected waiting car",
+	"Connected waiting schedule",
+	"Paused",
+	"Schedule end",
+	"Locked",
+	"Error",
+	"Connected waiting current assignation",
+	"Unconfigured power sharing",
+	"Queue by power boost",
+	"Discharging",
+	"Connected waiting admin auth for mid",
+	"Connected mid safety margin exceeded",
+	"OCPP unavailable",
+	"OCPP charge finishing",
+	"OCPP reserved",
+	"Updating",
+	"Queue by eco smart",
+}
+
+var stateOverrides = map[int]int{
+	0xA1: 0,
+	0xA2: 9,
+	0xA3: 14,
+	0xA4: 15,
+	0xA6: 17,
+	0xB1: 3,
+	0xB2: 4,
+	0xB3: 3,
+	0xB4: 2,
+	0xB5: 2,
+	0xB6: 4,
+	0xB7: 8,
+	0xB8: 8,
+	0xB9: 10,
+	0xBA: 10,
+	0xBB: 12,
+	0xBC: 13,
+	0xBD: 18,
+	0xC1: 1,
+	0xC2: 1,
+	0xC3: 11,
+	0xC4: 11,
+	0xD1: 6,
+	0xD2: 6,
+}
+
+var stateMachineStates = map[int]string{
+	0xE:  "Error",
+	0xF:  "Unviable",
+	0xA1: "Ready",
+	0xA2: "PS Unconfig",
+	0xA3: "Unavailable",
+	0xA4: "Finish",
+	0xA5: "Reserved",
+	0xA6: "Updating",
+	0xB1: "Connected 1", // Make new session?
+	0xB2: "Connected 2",
+	0xB3: "Connected 3", // Waiting schedule ?
+	0xB4: "Connected 4",
+	0xB5: "Connected 5", // Connected waiting car ?
+	0xB6: "Connected 6", // Paused
+	0xB7: "Waiting 1",
+	0xB8: "Waiting 2",
+	0xB9: "Waiting 3",
+	0xBA: "Waiting 4",
+	0xBB: "Mid 1",
+	0xBC: "Mid 2",
+	0xBD: "Waiting eco power",
+	0xC1: "Charging 1",
+	0xC2: "Charging 2",
+	0xC3: "Discharging 1",
+	0xC4: "Discharging 2",
+	0xD1: "Lock",
+	0xD2: "Wait Unlock",
+}
+
+var controlPilotStates = map[int]string{
+	0xE:  "Error",
+	0xF:  "Failure",
+	0xA1: "Ready 1", // S1 at 12V, car not connected
+	0xA2: "Ready 2",
+	0xB1: "Connected 1", // S1 at 9V, car connected not allowed charge
+	0xB2: "Connected 2", // S1 at Oscillator, car connected allowed charge
+	0xC1: "Charging 1",
+	0xC2: "Charging 2", // S2 closed
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..936f55e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,19 @@
+module github.com/jagheterfredrik/wallbox-mqtt-bridge
+
+go 1.19
+
+require (
+	github.com/eclipse/paho.mqtt.golang v1.4.3
+	github.com/go-sql-driver/mysql v1.7.1
+	github.com/jmoiron/sqlx v1.3.5
+	github.com/redis/go-redis/v9 v9.3.1
+	gopkg.in/ini.v1 v1.67.0
+)
+
+require (
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
+	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/sync v0.1.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..7dadc3f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,23 @@
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
+github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
+github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..533f9f1
--- /dev/null
+++ b/main.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"github.com/jagheterfredrik/wallbox-mqtt-bridge/app"
+	"os"
+)
+
+func main() {
+	if len(os.Args) != 2 {
+		panic("Usage: ./bridge --config or ./bridge bridge.ini")
+	}
+	firstArgument := os.Args[1]
+	if firstArgument == "--config" {
+		bridge.RunTuiSetup()
+		os.Exit(0)
+	} else {
+		bridge.LaunchBridge(firstArgument)
+	}
+}
diff --git a/make.sh b/make.sh
new file mode 100755
index 0000000..29a72a7
--- /dev/null
+++ b/make.sh
@@ -0,0 +1,4 @@
+set -x
+
+CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags="-s -w" -o bridge-armhf .
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bridge-arm64 .
diff --git a/mqtt-bridge/bridge.ini b/mqtt-bridge/bridge.ini
deleted file mode 100644
index 74ac06c..0000000
--- a/mqtt-bridge/bridge.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-[mqtt]
-host = 192.168.86.4
-port = 1883
-username = mqtt-user
-password = mqtt
-
-[settings]
-polling_interval_seconds = 1
-device_name = Wallbox
-legacy_locking = no
diff --git a/mqtt-bridge/bridge.py b/mqtt-bridge/bridge.py
deleted file mode 100644
index ed3105a..0000000
--- a/mqtt-bridge/bridge.py
+++ /dev/null
@@ -1,399 +0,0 @@
-"""MQTT bridge.
-
-Polls the local database and publishes changes to an external MQTT broker for Home Assistant.
-Accepts changes from Home Assistant and updates the local database.
-Supports Home Assistant discovery.
-"""
-import configparser
-import ctypes
-import json
-import logging
-import os
-import re
-import time
-from typing import Any, Dict  # noqa: F401
-
-import paho.mqtt.client as mqtt
-import pymysql.cursors
-import redis
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger("mqtt-bridge")
-
-WALLBOX_STATUS_CODES = [
-    "Ready",
-    "Charging",
-    "Connected waiting car",
-    "Connected waiting schedule",
-    "Paused",
-    "Schedule end",
-    "Locked",
-    "Error",
-    "Connected waiting current assignation",
-    "Unconfigured power sharing",
-    "Queue by power boost",
-    "Discharging",
-    "Connected waiting admin auth for mid",
-    "Connected mid safety margin exceeded",
-    "OCPP unavailable",
-    "OCPP charge finishing",
-    "OCPP reserved",
-    "Updating",
-    "Queue by eco smart",
-]
-
-STATE_OVERRIDES = {
-    0xA1: 0,
-    0xA2: 9,
-    0xA3: 14,
-    0xA4: 15,
-    0xA6: 17,
-    0xB1: 3,
-    0xB2: 4,
-    0xB3: 3,
-    0xB4: 2,
-    0xB5: 2,
-    0xB6: 4,
-    0xB7: 8,
-    0xB8: 8,
-    0xB9: 10,
-    0xBA: 10,
-    0xBB: 12,
-    0xBC: 13,
-    0xBD: 18,
-    0xC1: 1,
-    0xC2: 1,
-    0xC3: 11,
-    0xC4: 11,
-    0xD1: 6,
-    0xD2: 6,
-}
-
-connection = pymysql.connect(
-    host="localhost",
-    user="root",
-    password="fJmExsJgmKV7cq8H",
-    db="wallbox",
-    charset="utf8mb4",
-    cursorclass=pymysql.cursors.DictCursor,
-)
-# Because the transaction isolation is set to REPEATABLE-READ we need to commit after every write and read.
-# If it was READ-COMMITTED this would only be needed after every write.
-# Check the transaction isolation with:
-# "SELECT @@GLOBAL.tx_isolation, @@tx_isolation;"
-connection.autocommit(True)
-
-redis_connection = redis.Redis(host="localhost", port=6379, db=0)
-
-
-def sql_execute(sql, *args):
-    with connection.cursor() as cursor:
-        cursor.execute(sql, args)
-        return cursor.fetchone()
-
-
-def redis_hget(name, key):
-    result = redis_connection.hget(name, key)
-    assert result, name + " " + key + " not found in redis"
-    return result
-
-
-def redis_hmget(name, keys):
-    result = redis_connection.hmget(name, keys)
-    assert result
-    return result
-
-
-# Below, we're using arm64 syscalls to interact with Posix message queues
-# sysall 274 is mq_open(name, oflag, mode, attr)
-# sysall 276 is mq_timedsend(mqdes, msg_ptr, msg_len, msg_prio, abs_timeout)
-libc = ctypes.CDLL(None)
-syscall = libc.syscall
-
-
-def send_to_posix_queue(name, msg):
-    mq = syscall(274, name, 0x2, 0x1C7, None)
-    if mq < 0:
-        return
-    syscall(276, mq, msg.ljust(1024, b"\x00"), 1024, 0, None)
-    os.close(mq)
-
-
-def pause_resume(val):
-    proposed_state = int(val)
-    current_state = sql_execute("SELECT `charging_enable` FROM wallbox_config;")["charging_enable"]
-    if proposed_state == current_state:
-        return
-
-    if proposed_state == 1:
-        send_to_posix_queue(b"WALLBOX_MYWALLBOX_WALLBOX_STATEMACHINE", b"EVENT_REQUEST_USER_ACTION#1.000000")
-    elif proposed_state == 0:
-        send_to_posix_queue(b"WALLBOX_MYWALLBOX_WALLBOX_STATEMACHINE", b"EVENT_REQUEST_USER_ACTION#2.000000")
-
-
-# Needed for unlock
-wallbox_uid = sql_execute("SELECT `user_id` FROM `users` WHERE `user_id` != 1 ORDER BY `user_id` DESC LIMIT 1;")[
-    "user_id"
-]
-
-
-def lock_unlock(val):
-    proposed_state = int(val)
-    current_state = sql_execute("SELECT `lock` FROM wallbox_config;")["lock"]
-    if proposed_state == current_state:
-        return
-
-    if proposed_state == 1:
-        send_to_posix_queue(b"WALLBOX_MYWALLBOX_WALLBOX_LOGIN", b"EVENT_REQUEST_LOCK")
-    elif proposed_state == 0:
-        send_to_posix_queue(b"WALLBOX_MYWALLBOX_WALLBOX_LOGIN", (b"EVENT_REQUEST_LOGIN#%d.000000" % wallbox_uid))
-
-
-# Applies some additional rules to the internal state and returns the status as a string
-def effective_status_string():
-    tms_status = int(redis_hget("m2w", "tms.charger_status"))
-    state = int(redis_hget("state", "session.state"))
-    if state in STATE_OVERRIDES:
-        tms_status = STATE_OVERRIDES[state]
-    return WALLBOX_STATUS_CODES[tms_status]
-
-
-ENTITIES_CONFIG = {
-    "charging_enable": {
-        "component": "switch",
-        "setter": pause_resume,
-        "config": {
-            "name": "Charging enable",
-            "payload_on": 1,
-            "payload_off": 0,
-            "icon": "mdi:ev-station",
-        },
-    },
-    "lock": {
-        "component": "lock",
-        "setter": lock_unlock,
-        "config": {
-            "name": "Lock",
-            "payload_lock": 1,
-            "payload_unlock": 0,
-            "state_locked": 1,
-            "state_unlocked": 0,
-        },
-    },
-    "max_charging_current": {
-        "component": "number",
-        "setter": lambda val: sql_execute("UPDATE `wallbox_config` SET `max_charging_current`=%s;", val),
-        "config": {
-            "name": "Max charging current",
-            "min": 6,
-            "max": 40,
-            "unit_of_measurement": "A",
-            "device_class": "current",
-        },
-    },
-    "halo_brightness": {
-        "component": "number",
-        "setter": lambda val: sql_execute("UPDATE `wallbox_config` SET `halo_brightness`=%s;", val),
-        "config": {
-            "name": "Halo Brightness",
-            "min": 0,
-            "max": 100,
-            "icon": "mdi:brightness-percent",
-            "unit_of_measurement": "%",
-            "entity_category": "config",
-        },
-    },
-    "cable_connected": {
-        "component": "binary_sensor",
-        "getter": lambda: int(int(redis_hget("m2w", "tms.charger_status")) not in (0, 6)),
-        "config": {
-            "name": "Cable connected",
-            "payload_on": 1,
-            "payload_off": 0,
-            "icon": "mdi:ev-plug-type1",
-            "device_class": "plug",
-        },
-    },
-    "charging_power": {
-        "component": "sensor",
-        "getter": lambda: sum(
-            float(v)
-            for v in redis_hmget(
-                "m2w", ["tms.line1.power_watt.value", "tms.line2.power_watt.value", "tms.line3.power_watt.value"]
-            )
-        ),
-        "config": {
-            "name": "Charging power",
-            "device_class": "power",
-            "unit_of_measurement": "W",
-            "state_class": "total",
-            "suggested_display_precision": 1,
-        },
-    },
-    "status": {
-        "component": "sensor",
-        "getter": effective_status_string,
-        "config": {
-            "name": "Status",
-        },
-    },
-    "added_energy": {
-        "component": "sensor",
-        "getter": lambda: float(redis_hget("state", "scheduleEnergy")),
-        "config": {
-            "name": "Added energy",
-            "device_class": "energy",
-            "unit_of_measurement": "Wh",
-            "state_class": "total",
-            "suggested_display_precision": 1,
-        },
-    },
-    "cumulative_added_energy": {
-        "component": "sensor",
-        "config": {
-            "name": "Cumulative added energy",
-            "device_class": "energy",
-            "unit_of_measurement": "Wh",
-            "state_class": "total_increasing",
-            "suggested_display_precision": 1,
-        },
-    },
-    "added_range": {
-        "component": "sensor",
-        "config": {
-            "name": "Added range",
-            "device_class": "distance",
-            "unit_of_measurement": "km",
-            "state_class": "total",
-            "suggested_display_precision": 1,
-            "icon": "mdi:map-marker-distance",
-        },
-    },
-}  # type: Dict[str, Dict[str, Any]]
-
-DB_QUERY = """
-SELECT
-  `wallbox_config`.`charging_enable`,
-  `wallbox_config`.`lock`,
-  `wallbox_config`.`max_charging_current`,
-  `wallbox_config`.`halo_brightness`,
-  `power_outage_values`.`charged_energy` AS cumulative_added_energy,
-  IF(`active_session`.`unique_id` != 0,
-    `active_session`.`charged_range`,
-    `latest_session`.`charged_range`) AS added_range
-FROM `wallbox_config`,
-    `active_session`,
-    `power_outage_values`,
-    (SELECT * FROM `session` ORDER BY `id` DESC LIMIT 1) AS latest_session;
-"""
-
-mqttc = mqtt.Client()
-
-try:
-    config = configparser.ConfigParser()
-    config.read(os.path.join(os.path.dirname(__file__), "bridge.ini"))
-    mqtt_host = config.get("mqtt", "host")
-    mqtt_port = config.getint("mqtt", "port")
-    mqtt_username = config.get("mqtt", "username")
-    mqtt_password = config.get("mqtt", "password")
-    polling_interval_seconds = config.getfloat("settings", "polling_interval_seconds")
-    device_name = config.get("settings", "device_name")
-    if config.getboolean("settings", "legacy_locking", fallback=False):
-        ENTITIES_CONFIG["lock"]["setter"] = lambda val: sql_execute("UPDATE `wallbox_config` SET `lock`=%s;", val)
-
-    # Prepare the MQTT topic name to include the serial number of the Wallbox
-    result = sql_execute("SELECT `serial_num` FROM `charger_info`;")
-    assert result
-    serial_num = str(result["serial_num"])
-
-    # Set max available current
-    result = sql_execute("SELECT `max_avbl_current` FROM `state_values` ORDER BY `id` DESC LIMIT 1;")
-    assert result
-    ENTITIES_CONFIG["max_charging_current"]["config"]["max"] = result["max_avbl_current"]
-
-    topic_prefix = "wallbox_" + serial_num
-    set_topic = topic_prefix + "/+/set"
-    set_topic_re = re.compile(topic_prefix + "/(.*)/set")
-    availability_topic = topic_prefix + "/availability"
-
-    def _on_connect(client, userdata, flags, rc):
-        logger.info("Connected to MQTT with %d", rc)
-        if rc == mqtt.MQTT_ERR_SUCCESS:
-            mqttc.subscribe(set_topic)
-            mqttc.publish(availability_topic, "online", retain=True)
-            for k, v in ENTITIES_CONFIG.items():
-                unique_id = serial_num + "_" + k
-                component = v["component"]
-                config = {
-                    "~": topic_prefix + "/" + k,
-                    "state_topic": "~/state",
-                    "unique_id": unique_id,
-                    "device": {
-                        "identifiers": serial_num,
-                        "name": device_name,
-                    },
-                    "availability_topic": availability_topic,
-                }
-                if "setter" in v:
-                    config["command_topic"] = "~/set"
-                config = {**v["config"], **config}
-                mqttc.publish(
-                    "homeassistant/" + component + "/" + unique_id + "/config",
-                    json.dumps(config),
-                    retain=True,
-                )
-
-    def _on_message(client, userdata, message):
-        m = set_topic_re.match(message.topic)
-        if m:
-            field = m.group(1)
-            if field in ENTITIES_CONFIG and "setter" in ENTITIES_CONFIG[field]:
-                logger.info("Setting: %s %s", field, message.payload.decode())
-                ENTITIES_CONFIG[field]["setter"](message.payload)
-            else:
-                logger.info("Setting unsupported for field %s", field)
-
-    def on_disconnect(client, userdata, rc):
-        if rc != 0:
-            raise Exception("Disconnected")
-
-    mqttc.on_disconnect = on_disconnect
-    mqttc.on_connect = _on_connect
-    mqttc.on_message = _on_message
-    mqttc.username_pw_set(mqtt_username, mqtt_password)
-    mqttc.will_set(availability_topic, "offline", retain=True)
-    logger.info("Connecting to MQTT %s %s", mqtt_host, mqtt_port)
-    mqttc.connect(mqtt_host, mqtt_port)
-
-    published = {}  # type: Dict[str, Any]
-    # If we change more than this, we publish even though we're rate limited
-    rate_limit_deltas = {
-        "charging_power": 100,
-        "added_energy": 50,
-    }
-    rate_limit_s = 10.0
-    latest_rate_limit_publish = 0.0
-    while True:
-        if mqttc.is_connected():
-            result = sql_execute(DB_QUERY)
-            assert result
-            for k, v in ENTITIES_CONFIG.items():
-                if "getter" in v:
-                    result[k] = v["getter"]()
-            publish_rate_limited = latest_rate_limit_publish + rate_limit_s < time.time()
-            for key, val in result.items():
-                if published.get(key) != val:
-                    if key in rate_limit_deltas and not publish_rate_limited:
-                        if abs(published.get(key, 0) - val) < rate_limit_deltas[key]:
-                            continue
-                    logger.info("Publishing: %s %s", key, val)
-                    mqttc.publish(topic_prefix + "/" + key + "/state", val, retain=True)
-                    published[key] = val
-            if publish_rate_limited:
-                latest_rate_limit_publish = time.time()
-        mqttc.loop(timeout=polling_interval_seconds)
-
-finally:
-    # Intentionally not calling mqttc.disconnect() so that the broker sends the will (offline to the availability_topic)
-    connection.close()
-    redis_connection.close()
diff --git a/mqtt-bridge/install.sh b/mqtt-bridge/install.sh
deleted file mode 100755
index 98de99b..0000000
--- a/mqtt-bridge/install.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/bash
-
-if [ ! -d "$HOME/.wallbox" ]; then
-    echo "This script should only be run on a Wallbox!"
-    exit 1
-fi
-
-base_dir="/home/root/mqtt-bridge"
-if [ ! -d "$base_dir" ]; then
-    echo "$base_dir not found!"
-    exit 1
-fi
-files=("bridge.py" "bridge.ini" "mqtt-bridge.service" "requirements.txt")
-for f in "${files[@]}"; do
-    if [ ! -f "$base_dir/$f" ]; then
-        echo "$base_dir/$f not found!"
-        exit 1
-    fi
-done
-
-cd "$base_dir" || exit 1
-
-echo "Setting up virtual environment"
-python3 -m venv venv
-
-echo "Installing Python dependencies"
-venv/bin/pip install -r requirements.txt
-
-echo "Setting up the MQTT bridge systemd service"
-ln -s /home/root/mqtt-bridge/mqtt-bridge.service /lib/systemd/system/mqtt-bridge.service
-
-echo "Enable the service to start on boot.."
-systemctl enable mqtt-bridge
-
-echo "..and launch the service now"
-systemctl restart mqtt-bridge
-
-echo "Sleeping for 3 seconds before checking the status of the systemd service"
-sleep 3
-systemctl status mqtt-bridge --no-pager
diff --git a/mqtt-bridge/mqtt-bridge.service b/mqtt-bridge/mqtt-bridge.service
deleted file mode 100644
index 649bc3a..0000000
--- a/mqtt-bridge/mqtt-bridge.service
+++ /dev/null
@@ -1,15 +0,0 @@
-[Unit]
-Description=MQTT Bridge
-After=network.target
-Requires=mysqld.service
-StartLimitIntervalSec=0
-
-[Service]
-Type=simple
-Restart=always
-RestartSec=1
-User=root
-ExecStart=/home/root/mqtt-bridge/venv/bin/python3 /home/root/mqtt-bridge/bridge.py
-
-[Install]
-WantedBy=multi-user.target
diff --git a/mqtt-bridge/requirements.txt b/mqtt-bridge/requirements.txt
deleted file mode 100644
index a0b5ea9..0000000
--- a/mqtt-bridge/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-paho-mqtt==1.6.1
-pymysql==0.10.1
-redis==3.5.3
diff --git a/pyproject.toml b/pyproject.toml
deleted file mode 100644
index da4ca2e..0000000
--- a/pyproject.toml
+++ /dev/null
@@ -1,2 +0,0 @@
-[tool.ruff]
-line-length = 120