Skip to content

Commit

Permalink
Add MQTT output buffering and configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
bjpirt committed Dec 6, 2023
1 parent 7b35f51 commit 8095899
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 55 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ Which will produce `build/out/tesla-bms-emulator.uf2`. You can drag and drop thi
## To Do

- [ ] Add support for Ethernet networking
- [ ] Update settings via MQTT
- [ ] Output buffering for MQTT (send on change)
- [x] Update settings via MQTT
- [x] Output buffering for MQTT (send on change)
- [x] Add support for heating the battery pack in cold weather
- [x] Dynamically scale back the charge current to avoid going over voltage
- [x] Add hysteresis on over voltage faults
Expand Down
6 changes: 3 additions & 3 deletions bms/battery_heating.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

class BatteryHeating:
def __init__(self, config: Config, pack: BatteryPack) -> None:
self.set_temperature = config.battery_heating_temperature
self.__config = config
self.__pack = pack
self.__pin = Pin(config.battery_heating_pin, Pin.OUT)
self.heating = False

def process(self):
if self.__pack.low_temperature < self.set_temperature:
if self.__pack.low_temperature < self.__config.battery_heating_temperature:
self.__pin.on()

if self.__pack.low_temperature > self.set_temperature + 1:
if self.__pack.low_temperature > self.__config.battery_heating_temperature + 1:
self.__pin.off()
101 changes: 72 additions & 29 deletions bms/mqtt_output.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# ruff: noqa: E722
from hal.network import NetworkConnectionInterface
from .bms_interface import BmsInterface
from hal import get_interval
from hal import get_interval, Memory
from config import Config
from mqtt import MQTTClient # type: ignore
import json
from typing import Union
from typing import Dict, Union
try:
import uasyncio as asyncio # type: ignore
except ImportError:
Expand All @@ -16,57 +17,99 @@ def __init__(self, config: Config, bms: BmsInterface, network: NetworkConnection
self._config = config
self.enabled = self._config.mqtt_enabled is True
self.connected = False
self._memory = Memory()
self._last_values: Dict[str, Union[str, int, float, None]] = {}
self._full_publish_interval = get_interval()
self._full_publish_interval.set(self._config.mqtt_full_output_interval)
if self.enabled:
self._bms = bms
self._network = network
self._client = MQTTClient("pyBms", self._config.mqtt_host)
self._interval = get_interval()
self._interval.set(self._config.mqtt_output_interval)
self._client.set_callback(self._sub_cb)
self._publish_interval = get_interval()
self._publish_interval.set(self._config.mqtt_output_interval)

def _connect(self) -> None:
if not self.connected and self._network.connected:
try:
print(f"Connecting to MQTT server: {self._config.mqtt_host}")
self._client.connect()
self._client.subscribe(f"{self._config.mqtt_topic_prefix}/set-config/#")
print("Connected to MQTT")
self.connected = True
except OSError:
self.connected = False
print("Failed to connect to MQTT")

def _publish(self):
if self._interval.ready and self._network.connected:
self._connect()
self._interval.reset()
if self.connected:
self._publish_topic("/voltage", self._bms.battery_pack.voltage)
self._publish_topic("/soc", self._bms.state_of_charge)
for module_index, module in enumerate(self._bms.battery_pack.modules):
if self._publish_interval.ready and self._network.connected:
self._publish_bms_data()
self._publish_config()
self._publish_stats()

def _publish_bms_data(self):
self._connect()
self._publish_interval.reset()
if self.connected:
self._publish_topic("/voltage", self._bms.battery_pack.voltage)
self._publish_topic("/soc", self._bms.state_of_charge)
for module_index, module in enumerate(self._bms.battery_pack.modules):
self._publish_topic(
f"/modules/{module_index}/voltage", module.voltage)
self._publish_topic(f"/modules/{module_index}/fault", int(module.fault))
self._publish_topic(f"/modules/{module_index}/alert", int(module.alert))
for temp_index, temp in enumerate(module.temperatures):
self._publish_topic(
f"/modules/{module_index}/temperature/{temp_index}", temp)
for cell_index, cell in enumerate(module.cells):
self._publish_topic(
f"/modules/{module_index}/cells/{cell_index}/voltage", cell.voltage)
self._publish_topic(
f"/modules/{module_index}/cells/{cell_index}/fault", int(cell.fault))
self._publish_topic(
f"/modules/{module_index}/cells/{cell_index}/alert", int(cell.alert))
self._publish_topic(
f"/modules/{module_index}/voltage", module.voltage)
self._publish_topic(f"/modules/{module_index}/fault", int(module.fault))
self._publish_topic(f"/modules/{module_index}/alert", int(module.alert))
for temp_index, temp in enumerate(module.temperatures):
self._publish_topic(
f"/modules/{module_index}/temperature/{temp_index}", temp)
for cell_index, cell in enumerate(module.cells):
self._publish_topic(
f"/modules/{module_index}/cells/{cell_index}/voltage", cell.voltage)
self._publish_topic(
f"/modules/{module_index}/cells/{cell_index}/fault", int(cell.fault))
self._publish_topic(
f"/modules/{module_index}/cells/{cell_index}/alert", int(cell.alert))
self._publish_topic(
f"/modules/{module_index}/cells/{cell_index}/balancing", int(cell.balancing))
f"/modules/{module_index}/cells/{cell_index}/balancing", int(cell.balancing))

def _publish_config(self):
for (key, value) in self._config.get_dict().items():
self._publish_topic(f"/config/{key}", value)

def _publish_stats(self):
self._publish_topic("/memory/free", self._memory.free)
self._publish_topic("/memory/alloc", self._memory.alloc)

def _should_publish(self, topic: str, value: Union[int, bool, float]) -> bool:
if topic not in self._last_values or value != self._last_values[topic]:
self._last_values[topic] = value
return True
return False

def _publish_topic(self, topic: str, value: Union[int, bool, float]) -> None:
self._client.publish(f"{self._config.mqtt_topic_prefix}{topic}", json.dumps(
{"value": value}))
if self._should_publish(topic, value):
to_send = json.dumps({"value": value})
self._client.publish(f"{self._config.mqtt_topic_prefix}{topic}", to_send)

def _sub_cb(self, topic: str, msg) -> None:
try:
setting = topic.replace(f"{self._config.mqtt_topic_prefix}/set-config/", "")
data = json.loads(msg)
print(f"Setting '{setting}' to '{data['value']}'")
self._config.set_value(setting, data["value"])
self._config.save()
except: # pylint: disable=bare-except
print("Error decoding json from MQTT")

async def main(self):
while True:
if self.enabled and self._network.connected:
self._publish()
await asyncio.sleep_ms(1)
try:
await asyncio.sleep_ms(1)
except: # pylint: disable=bare-except
await asyncio.sleep(0.001)
if self.connected:
self._client.check_msg()
if self._full_publish_interval.ready:
self._full_publish_interval.reset()
self._last_values = {}
2 changes: 1 addition & 1 deletion config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"precharge_pin": 17,
"positive_pin": 26,
"balancing_enabled": true,
"balance_time": 5,
"balance_time": 5.0,
"balance_voltage": 3.9,
"balance_difference": 0.04,
"module_capacity": 232.0,
Expand Down
35 changes: 24 additions & 11 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
from typing import Union
import json


Expand Down Expand Up @@ -93,14 +92,6 @@ def __init__(self, file="config.json"):
self.current_reversed: bool = False
# The maximum amps for the full reading of the current sensor
self.current_sensor_max: int = 200
# Whether MQTT is enabled
self.mqtt_enabled: bool = False
# The host for the MQTT broker
self.mqtt_host: Union[str, None] = None
# The host for the MQTT broker
self.mqtt_topic_prefix: Union[str, None] = None
# The host for the MQTT broker
self.mqtt_output_interval: float = 5.0
# The max desired charge current in A
self.max_charge_current: float = 100.0
# The max desired discharge current in A
Expand All @@ -122,6 +113,20 @@ def __init__(self, file="config.json"):
# Use the current sensor for state of charge
self.current_sensor_soc = False

######################################################################
# MQTT Settings
######################################################################
# Whether MQTT is enabled
self.mqtt_enabled: bool = False
# The host for the MQTT broker
self.mqtt_host: str = ""
# The host for the MQTT broker
self.mqtt_topic_prefix: str = ""
# The host for the MQTT broker
self.mqtt_output_interval: float = 5.0
# How long between sending a full update
self.mqtt_full_output_interval: float = 120.0

self.read()

@property
Expand Down Expand Up @@ -160,10 +165,18 @@ def read(self):
new_config = json.loads(data)
self.update(new_config)

def set_value(self, key: str, value):
if hasattr(self, key):
if type(getattr(self, key)) == type(value) or type(value) == type(None):
setattr(self, key, value)
else:
print(f"Config types did not match: {key} ({type(getattr(self, key))}) ({type(value)})")
else:
print("Attribute does not exist")

def update(self, new_config: dict) -> None:
for (key, value) in new_config.items():
if hasattr(self, key):
setattr(self, key, value)
self.set_value(key, value)

def save(self):
with open(self.__file, 'w', encoding="utf-8") as file:
Expand Down
1 change: 1 addition & 0 deletions hal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from .pin import Pin as Pin
from .wdt import WDT as WDT
from .i2c import I2C as I2C
from .memory import Memory as Memory
19 changes: 19 additions & 0 deletions hal/memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# pylint: disable=unused-import
# pylint: disable=no-member
# ruff: noqa: F401
import gc
MPY = True
try:
import machine # type: ignore
except ModuleNotFoundError:
MPY = False


class Memory:
@property
def free(self):
return gc.mem_free() if MPY else 0 # type: ignore[attr-defined]

@property
def alloc(self):
return gc.mem_alloc() if MPY else 0 # type: ignore[attr-defined]
2 changes: 1 addition & 1 deletion hal/network/wifi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ async def connect(self):

@property
def connected(self):
return self.wlan.isconnected() if MPY and self.wlan else False
return self.wlan.isconnected() if MPY and self.wlan else True
2 changes: 1 addition & 1 deletion mqtt/paho_mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ def check_msg(self):

def _on_message(self, client, userdata, msg):
if self._callback:
self._callback(msg.topic, str(msg.payload))
self._callback(msg.topic, msg.payload.decode("utf-8"))
12 changes: 7 additions & 5 deletions platforms/cpython/pyBms.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
# ruff: noqa: E402
from config import Config
import sys
import serial # type: ignore
from os import path # type: ignore
from os import path


sys.path.insert(0, path.join(path.dirname(__file__), "../.."))

from bms import Bms, WebServer # nopep8
from bms import Bms, Network # nopep8
from battery.tesla_model_s import TeslaModelSBatteryPack, TeslaModelSNetworkGateway # nopep8
from config import Config # nopep8

port = sys.argv[1]
baud = int(sys.argv[2])

serialPort = serial.Serial(port, baud, timeout=0.1)
config = Config()
config = Config("config.local.json")
gateway = TeslaModelSNetworkGateway(serialPort, config)
pack = TeslaModelSBatteryPack(gateway, config)
bms = Bms(pack, config)
webServer = WebServer(bms, config)
# webServer = WebServer(bms, config)
Network(bms, config)


def main():
Expand Down
2 changes: 0 additions & 2 deletions platforms/esp32/wemos-d1/pyBms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from bms import Bms, VictronOutput, Network, C2TTransducer
from bms.web_server import WebServer
from config import Config
from battery.tesla_model_s import TeslaModelSBatteryPack, TeslaModelSNetworkGateway
from machine import UART # type: ignore
Expand All @@ -19,7 +18,6 @@ def main():
Network(bms, config)
victron_output = VictronOutput(can, bms, 0.5)
heating_control = BatteryHeating(config, pack)
WebServer(bms, config)

while True:
bms.process()
Expand Down

0 comments on commit 8095899

Please sign in to comment.