li3: decode status flags, fix info-sensor expire_after, restart bluetooth after first failed scan
- status_text: decode the 24-bit "status" field into human-readable flag names (e.g. "Cell Temp Low, Low Voltage") instead of raw hex, using the bit table pulled from com.lithionics.bms's array/advanced resource (dumped with aapt -- the app's own StatusCodeTable class references stale/wrong resource IDs and can't be trusted for this). Applied to "status" only; last_fault_code is a lifetime latch that accumulates many bits over time and isn't meaningfully summarized the same way. - Fixed the info sensors (total_consumed, firmware_version, serial_number, etc.) showing "Unavailable" in HA a minute after connecting: they were getting the same expire_after as the periodic telemetry sensors, but the $info line the come from is only sent once per BLE connection, so HA's expire timer always fired. These are retained-topic diagnostics meant to hold their last value indefinitely; expire_after now only applies to the periodic SENSORS group. - STUCK_DISCOVERY_THRESHOLD 3 -> 1: restart bluetooth.service after the first failed scan instead of waiting for three, cutting reconnect time roughly in third. Still rate-limited via BLUETOOTH_RESTART_COOLDOWN_S. Also removed the per-line "RAW ..." debug print (was flooding the journal). All three changes verified live on host wan via journalctl/mosquitto_sub.
This commit is contained in:
+51
-14
@@ -11,10 +11,11 @@
|
||||
# occasionally wedges its discovery state after a run of failed connection
|
||||
# attempts (Discovering stays "yes" forever, and every subsequent connect
|
||||
# fails with le-connection-abort-by-local) — observed happening from our own
|
||||
# repeated scan/connect cycling, not anything external. After several
|
||||
# consecutive failures we restart bluetooth.service ourselves to clear that,
|
||||
# rate-limited so we don't do it so often it disrupts the other BLE gear on
|
||||
# this hub (motion sensors, IR remote, etc. also served by bluetoothd here).
|
||||
# repeated scan/connect cycling, not anything external. We restart
|
||||
# bluetooth.service ourselves as soon as a scan fails to clear that, rate-
|
||||
# limited (BLUETOOTH_RESTART_COOLDOWN_S) so we don't do it so often it
|
||||
# disrupts the other BLE gear on this hub (motion sensors, IR remote, etc.
|
||||
# also served by bluetoothd here).
|
||||
#
|
||||
# Only exits (letting systemd Restart=always give us a fresh process) on
|
||||
# MQTT-level failure, which shouldn't happen in normal operation.
|
||||
@@ -29,7 +30,7 @@ import paho.mqtt.client as mqtt
|
||||
from bleak import BleakClient, BleakScanner
|
||||
|
||||
BLE_RETRY_DELAY_S = 8
|
||||
STUCK_DISCOVERY_THRESHOLD = 3 # consecutive scan failures before we intervene
|
||||
STUCK_DISCOVERY_THRESHOLD = 1 # consecutive scan failures before we intervene
|
||||
BLUETOOTH_RESTART_COOLDOWN_S = 300 # don't restart bluetooth.service more than this often
|
||||
|
||||
config_path = sys.argv[1] if len(sys.argv) > 1 else "/etc/van-li3/config.json"
|
||||
@@ -94,6 +95,37 @@ INFO_SENSORS = [
|
||||
|
||||
info_topic = cfg.get("info_topic", f"van/{device_id}/info")
|
||||
|
||||
# The "status" field (main Cs telemetry line, and identically-coded but
|
||||
# cumulative/latched "last_fault_code" from $info) is a 24-bit flag mask.
|
||||
# Bit meanings pulled from the app's own "advanced" string-array resource
|
||||
# (com.lithionics.bms base.apk, array/advanced -- dumped with aapt since the
|
||||
# app's Kotlin StatusCodeTable class references stale/wrong resource IDs and
|
||||
# can't be trusted). Index 0 = bit 23 (MSB) down to index 23 = bit 0 (LSB);
|
||||
# blank entries are unused bits. Applied to "status" only -- last_fault_code
|
||||
# is a lifetime latch (many bits accumulate over time) and isn't meaningfully
|
||||
# summarized the same way.
|
||||
STATUS_FLAGS = [
|
||||
"", "BMS Temp High", "Overcurrent State", "Charge OFF", "Aux Input State",
|
||||
"Cell Temp Low", "Cell Temp High", "AGSR State", "Temp Sensor Fault",
|
||||
"CAN Charger Fault", "CAN Charger Present", "AC Power Present",
|
||||
"Contactor Flutter", "Pre-Charge Fault", "Contactor Fault",
|
||||
"Contactor State", "Power Off State", "Battery Protection", "Low Voltage",
|
||||
"Reserve Range", "OptoLoop Open", "NeverDie Reserve", "Charge Detected",
|
||||
"High Voltage",
|
||||
]
|
||||
|
||||
|
||||
def decode_status(hex_code):
|
||||
try:
|
||||
value = int(hex_code, 16)
|
||||
except (ValueError, TypeError):
|
||||
return hex_code
|
||||
active = [
|
||||
label for i, label in enumerate(STATUS_FLAGS)
|
||||
if label and (value >> (23 - i)) & 1
|
||||
]
|
||||
return ", ".join(active) if active else "OK"
|
||||
|
||||
|
||||
def parse_line(line):
|
||||
"""Cs-series main telemetry line (no prefix character)."""
|
||||
@@ -158,7 +190,7 @@ def parse_info_line(line):
|
||||
return None
|
||||
|
||||
|
||||
def _sensor_config(key, name, unit, device_class, precision, topic_state, device_info):
|
||||
def _sensor_config(key, name, unit, device_class, precision, topic_state, device_info, expire_after=None):
|
||||
numeric = precision is not None
|
||||
payload = {
|
||||
"name": name,
|
||||
@@ -172,8 +204,9 @@ def _sensor_config(key, name, unit, device_class, precision, topic_state, device
|
||||
else f"{{{{ value_json.{key} }}}}"
|
||||
),
|
||||
"device": device_info,
|
||||
"expire_after": publish_interval_s * 4,
|
||||
}
|
||||
if expire_after is not None:
|
||||
payload["expire_after"] = expire_after
|
||||
if numeric:
|
||||
payload["suggested_display_precision"] = precision
|
||||
payload["state_class"] = "measurement"
|
||||
@@ -189,7 +222,10 @@ def publish_discovery(mqc):
|
||||
}
|
||||
for key, name, unit, device_class, precision in SENSORS:
|
||||
topic = f"{discovery_prefix}/sensor/{device_id}/{key}/config"
|
||||
payload = _sensor_config(key, name, unit, device_class, precision, state_topic, device_info)
|
||||
payload = _sensor_config(
|
||||
key, name, unit, device_class, precision, state_topic, device_info,
|
||||
expire_after=publish_interval_s * 4,
|
||||
)
|
||||
mqc.publish(topic, json.dumps(payload), retain=True)
|
||||
|
||||
status_topic = f"{discovery_prefix}/sensor/{device_id}/status/config"
|
||||
@@ -197,7 +233,7 @@ def publish_discovery(mqc):
|
||||
"name": "Status",
|
||||
"unique_id": f"{device_id}_status",
|
||||
"state_topic": state_topic,
|
||||
"value_template": "{{ 'OK' if value_json.status == '000000' else value_json.status }}",
|
||||
"value_template": "{{ value_json.status_text }}",
|
||||
"device": device_info,
|
||||
"expire_after": publish_interval_s * 4,
|
||||
}
|
||||
@@ -223,8 +259,9 @@ def build_state_payload(cs_reading, trace_fields):
|
||||
# cell4_voltage=34013.63) — publish only the status in that case. Trace
|
||||
# ("&" line) fields come from a separate message and are published
|
||||
# regardless.
|
||||
if cs_reading["status"] != "000000":
|
||||
payload = {"status": cs_reading["status"]}
|
||||
status = cs_reading["status"]
|
||||
if status != "000000":
|
||||
payload = {"status": status}
|
||||
else:
|
||||
payload = {
|
||||
"voltage": cs_reading["voltage"],
|
||||
@@ -236,14 +273,15 @@ def build_state_payload(cs_reading, trace_fields):
|
||||
"soc": cs_reading["soc_pct"],
|
||||
"bms_temperature": cs_reading["bms_temp_f"],
|
||||
"battery_temperature": cs_reading["batt_temp_f"],
|
||||
"status": cs_reading["status"],
|
||||
"status": status,
|
||||
}
|
||||
payload["status_text"] = decode_status(status)
|
||||
payload.update(trace_fields)
|
||||
return payload
|
||||
|
||||
|
||||
def restart_bluetooth_service():
|
||||
print("too many consecutive scan failures; restarting bluetooth.service")
|
||||
print("scan failed; restarting bluetooth.service")
|
||||
subprocess.run(["systemctl", "restart", "bluetooth.service"], check=False)
|
||||
time.sleep(3)
|
||||
|
||||
@@ -285,7 +323,6 @@ async def stream_from_device(dev, mqc):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
print("RAW", line)
|
||||
if line.startswith("&"):
|
||||
trace = parse_trace_line(line)
|
||||
if trace:
|
||||
|
||||
Reference in New Issue
Block a user