Move to python logging

This commit is contained in:
Odd Stråbø 2023-12-16 23:36:53 +01:00
parent 5524d16f68
commit d38abe28ba
5 changed files with 95 additions and 62 deletions

View file

@ -2,13 +2,17 @@
# -*- coding: utf-8 -*-
import time
from logging import getLogger
from logging.config import dictConfig as loggingDictConfig
from bluepy.btle import BTLEDisconnectError # type: ignore
from serial import SerialException # type: ignore
from .config import get_config, get_consumers, get_interface
from .protocol import ChargeController
from .util import Periodical, log
from .util import Periodical
logger = getLogger(__name__)
class CommunicationError(BTLEDisconnectError, SerialException, IOError):
@ -17,25 +21,24 @@ class CommunicationError(BTLEDisconnectError, SerialException, IOError):
def main():
conf = get_config()
loggingDictConfig(conf.get("logging", {}))
consumers = get_consumers(conf)
per_voltages = Periodical(interval=15)
per_current_hist = Periodical(interval=60)
# import serial
# ser = serial.Serial()
try:
while True:
try:
log("Connecting...")
logger.info("Connecting...")
with get_interface() as dev:
log("Connected.")
logger.info("Connected.")
cc = ChargeController(dev)
log(f"Controller model: {cc.model}")
log(f"Controller version: {cc.version}")
log(f"Controller serial: {cc.serial}")
logger.info(f"Controller model: {cc.model}")
logger.info(f"Controller version: {cc.version}")
logger.info(f"Controller serial: {cc.serial}")
for consumer in consumers:
consumer.controller = cc
@ -59,7 +62,7 @@ def main():
for i in range(min(days, 4)):
hist = cc.get_historical(i)
res = hist.as_dict()
log({i: res})
logger.debug({i: res})
for consumer in consumers:
consumer.write({str(i): res})
@ -68,14 +71,14 @@ def main():
if per_voltages(now):
data = cc.state.as_dict()
log(data)
logger.debug(data)
for consumer in consumers:
consumer.write(data)
if per_current_hist(now):
data = cc.today.as_dict()
data.update(cc.extra.as_dict())
log(data)
logger.debug(data)
for consumer in consumers:
consumer.write(data)
@ -91,7 +94,7 @@ def main():
# write(wd, CMD_ENABLE_LOAD)
except CommunicationError:
log("ERROR: Disconnected")
logger.error("Disconnected")
time.sleep(1)
except (KeyboardInterrupt, SystemExit, Exception) as e:

View file

@ -27,6 +27,29 @@ def get_config() -> Dict[str, Any]:
with open("config.yaml", "r") as fh:
conf: dict = yaml.safe_load(fh)
conf.setdefault("consumers", {})
logging = conf.setdefault("logging", {})
logging.setdefault("version", 1)
logging.setdefault("disable_existing_loggers", False)
logging.setdefault(
"handlers",
{
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"level": "INFO",
"stream": "ext://sys.stdout",
}
},
)
logging.setdefault(
"formatters",
{
"format": "%(asctime)s %(levelname)-8s %(name)-15s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
)
loggers = logging.setdefault("loggers", {})
loggers.setdefault("root", {"handlers": ["console"], "level": "DEBUG"})
return conf
@ -34,7 +57,9 @@ def get_config() -> Dict[str, Any]:
def write_config(conf: Dict[str, Any]):
with open(".config.yaml~writing", "w") as fh:
yaml.safe_dump(conf, fh, indent=2, encoding="utf-8")
os.rename(".config.yaml~writing", "config.yaml")
fh.flush()
os.fsync(fh.fileno())
os.replace(".config.yaml~writing", "config.yaml")
def get_consumers(conf: Optional[Dict[str, Any]] = None) -> List[BaseConsumer]:

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import json
from logging import getLogger
from time import sleep
from typing import Any, Dict, List, Optional, TypeAlias
from uuid import uuid4
@ -9,6 +10,8 @@ import paho.mqtt.client as mqtt
from ..solar_types import DataName
from . import BaseConsumer
logger = getLogger(__name__)
MAP_VALUES: Dict[DataName, Dict[str, Any]] = {
# DataName.BATTERY_VOLTAGE_MIN: {},
# DataName.BATTERY_VOLTAGE_MAX: {},
@ -161,8 +164,11 @@ class MqttConsumer(BaseConsumer):
elif err.errno == -3:
pass
else:
logger.exception("Unknown error connecting to mqtt server")
raise
print(err)
logger.warning(
"Temporary failure connecting to mqtt server", exc_info=True
)
sleep(0.1)
return self._client
@ -245,7 +251,7 @@ class MqttConsumer(BaseConsumer):
# The callback for when the client receives a CONNACK response from the server.
@staticmethod
def on_connect(client: mqtt.Client, userdata: "MqttConsumer", flags, rc):
print("Connected with result code " + str(rc))
logger.info("MQTT connected with result code %s", rc)
# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
@ -263,10 +269,7 @@ class MqttConsumer(BaseConsumer):
client: mqtt.Client, userdata: "MqttConsumer", message: mqtt.MQTTMessage
):
assert userdata.controller is not None
print(message)
print(message.info)
print(message.state)
print(message.payload)
logger.debug(message.payload)
payload = message.payload.decode().upper() in ("ON", "TRUE", "ENABLE", "YES")
res = userdata.controller.load_enabled = payload
@ -276,29 +279,29 @@ class MqttConsumer(BaseConsumer):
@staticmethod
def on_connect_fail(client: mqtt.Client, userdata: "MqttConsumer"):
print(userdata.__class__.__name__, "on_connect_fail")
logger.warning("on_connect_fail")
# The callback for when a PUBLISH message is received from the server.
@staticmethod
def on_message(client, userdata, msg):
print(msg.topic + " " + str(msg.payload))
logger.info(msg.topic + " " + str(msg.payload))
@staticmethod
def on_disconnect(client: mqtt.Client, userdata: "MqttConsumer", rc, prop=None):
print(userdata.__class__.__name__, "on_disconnect", rc)
logger.warning("on_disconnect %s", rc)
def poll(self):
res = self.client.loop(timeout=0.1, max_packets=5)
if res != mqtt.MQTT_ERR_SUCCESS:
print(self.__class__.__name__, "loop returned non-success:", res)
logger.warning("loop returned non-success: %s", res)
try:
sleep(1)
res = self.client.reconnect()
if res != mqtt.MQTT_ERR_SUCCESS:
print(self.__class__.__name__, "Reconnect failed:", res)
logger.error("Reconnect failed: %s", res)
except (OSError, mqtt.WebsocketConnectionError) as err:
print(self.__class__.__name__, "Reconnect failed:", err)
logger.error("Reconnect failed: %s", err)
return super().poll()

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
import struct
import sys
import time
from typing import Callable, Collection, Optional
from logging import getLogger
from typing import Callable, Collection, List, Optional
from libscrc import modbus # type: ignore
@ -16,7 +16,8 @@ from .solar_types import (
HistoricalData,
HistoricalExtraInfo,
)
from .util import log
logger = getLogger(__name__)
def write(fh, data):
@ -92,15 +93,13 @@ def discardUntil(fh: BaseInterface, byte: int, timeout=10) -> Optional[int]:
return b[0]
start = time.time()
discarded = 0
discarded: List[str] = []
read_byte = expand(fh.read(1))
while read_byte != byte:
if read_byte is not None:
if not discarded:
log("Discarding", end="")
discarded += 1
print(f" {read_byte:02X}", end="")
sys.stdout.flush()
discarded.append("Discarding")
discarded.append(f"{read_byte:02X}")
if time.time() - start > timeout:
read_byte = None
@ -109,8 +108,7 @@ def discardUntil(fh: BaseInterface, byte: int, timeout=10) -> Optional[int]:
read_byte = expand(fh.read(1))
if discarded:
print()
sys.stdout.flush()
logger.debug(" ".join(discarded))
return read_byte
@ -134,14 +132,18 @@ def readMemory(fh: BaseInterface, address: int, words: int = 1) -> Optional[byte
try:
crc = struct.unpack_from("<H", _crc)[0]
except struct.error:
log(f"readMemory: CRC error; read {len(_crc)} bytes (2 expected)")
logger.error(
"readMemory: CRC error; read %s bytes (2 expected)", len(_crc)
)
return None
calculated_crc = modbus(bytes([tag, operation, size, *data]))
if crc == calculated_crc:
return data
else:
log(f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}")
log("data or crc is falsely", header, data, _crc)
logger.error(
f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}"
)
logger.error("data or crc is falsely %s %s %s", header, data, _crc)
return None
@ -166,7 +168,7 @@ def writeMemory(fh: BaseInterface, address: int, data: bytes):
header = fh.read(3)
if header and len(header) == 3:
operation, size, address = header
log(header)
logger.log(5, header)
# size field is zero when writing device name for whatever reason
# write command seems to only accept a single word, so this is fine;
# we just hardcode the number of bytes read to two here.
@ -176,14 +178,18 @@ def writeMemory(fh: BaseInterface, address: int, data: bytes):
try:
crc = struct.unpack_from("<H", _crc)[0]
except struct.error:
log(f"writeMemory: CRC error; read {len(_crc)} bytes (2 expected)")
logger.error(
f"writeMemory: CRC error; read {len(_crc)} bytes (2 expected)"
)
return None
calculated_crc = modbus(bytes([tag, operation, size, address, *rdata]))
if crc == calculated_crc:
return rdata
else:
log(f"writeMemory: CRC error; {crc:04X} != {calculated_crc:04X}")
log("data or crc is falsely", header, rdata, _crc)
logger.error(
f"writeMemory: CRC error; {crc:04X} != {calculated_crc:04X}"
)
logger.error("data or crc is falsely %s %s %s", header, rdata, _crc)
return None
@ -193,7 +199,6 @@ def writeMemoryMultiple(fh: BaseInterface, address: int, data: bytes):
res = bytearray()
for i in range(len(data) // 2):
d = data[i * 2 : (i + 1) * 2]
log(address + i, d)
r = writeMemory(fh, address + i, d)
if r:
res.extend(r)
@ -214,15 +219,16 @@ def try_read_parse(
try:
if parser:
return parser(res)
except struct.error as e:
log(e)
log("0x0100 Unpack error:", len(res), res)
except struct.error:
logger.exception("0x0100 Unpack error: %s %s", len(res), res)
_timeout = dev.timeout
dev.timeout = 0.5
log("Flushed from read buffer; ", dev.read())
logger.warning("Flushed from read buffer; %s", dev.read())
dev.timeout = _timeout
else:
log(f"No data read, expected {words*2} bytes (attempts left: {attempts})")
logger.warning(
f"No data read, expected {words*2} bytes (attempts left: {attempts})"
)
return None
@ -308,7 +314,6 @@ class ChargeController:
# Pad name to 32 bytes to ensure ensure nothing is left of old name
while len(bin_value) < 32:
bin_value.extend(b"\x00\x20")
print(len(bin_value), bin_value)
data = writeMemoryMultiple(self.device, 0x0049, bin_value)
if data is None:
@ -316,7 +321,7 @@ class ChargeController:
res = data.decode("UTF-16BE").strip()
if res != value:
log(f"setting device name failed; {res!r} != {value!r}")
logger.error("setting device name failed; %r != %r", res, value)
self._cached_name = value
@property
@ -333,9 +338,9 @@ class ChargeController:
if data is not None:
res = struct.unpack("x?", data)[0]
if res != value:
log(f"setting load_enabled failed; {res!r} != {value!r}")
logger.error("setting load_enabled failed; %r != %r", res, value)
else:
log("setting load_enabled failed; communications error")
logger.error("setting load_enabled failed; communications error")
@property
def state(self) -> ChargerState:

View file

@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
import datetime
import sys
import time
from logging import getLogger
from typing import Optional
# Only factor of 1000
SI_PREFIXES_LARGE = "kMGTPEZY"
SI_PREFIXES_SMALL = "mµnpfazy"
logger = getLogger(__name__)
def humanize_number(data, unit: str = ""):
counter = 0
@ -35,11 +35,6 @@ def humanize_number(data, unit: str = ""):
return f"{data:.3g} {prefix}{unit}"
def log(*message: object, **kwargs):
print(datetime.datetime.utcnow().isoformat(" "), *message, **kwargs)
sys.stdout.flush()
class Periodical:
prev: float
interval: float
@ -56,7 +51,9 @@ class Periodical:
skipped, overshoot = divmod(now - self.prev, self.interval)
skipped -= 1
if skipped:
log("Skipped:", skipped, overshoot, now - self.prev, self.interval)
logger.debug(
"Skipped:", skipped, overshoot, now - self.prev, self.interval
)
self.prev = now - overshoot
return True