diff --git a/.pylintrc b/.pylintrc index 1587017..60da777 100644 --- a/.pylintrc +++ b/.pylintrc @@ -437,7 +437,7 @@ valid-metaclass-classmethod-first-arg=cls [DESIGN] # Maximum number of arguments for function / method. -max-args=12 +max-args=15 # Maximum number of attributes for a class (see R0902). max-attributes=15 @@ -452,7 +452,7 @@ max-branches=12 max-locals=20 # Maximum number of parents for a class (see R0901). -max-parents=7 +max-parents=10 # Maximum number of public methods for a class (see R0904). max-public-methods=20 diff --git a/setup.py b/setup.py index a3ac5d7..2da8249 100644 --- a/setup.py +++ b/setup.py @@ -43,17 +43,34 @@ install_requires=[ "packaging", "serviceinstaller >= 0.1.3 ; sys_platform=='linux'", + "simpleeval", "toml", ], extras_require={ "all": [ + "Adafruit-Blinka", + "adafruit-circuitpython-busdevice", + "gpiozero", "pymodbus", "pyserial", + "RPi.GPIO", + "smbus2", + ], + "adafruit": [ + "Adafruit-Blinka", + "adafruit-circuitpython-busdevice", + ], + "gpio": [ + "gpiozero", + "RPi.GPIO", ], "modbus": [ "pymodbus", "pyserial", ], + "smbus": [ + "smbus2", + ], }, entry_points={ "console_scripts": [ diff --git a/src/brokkr/.pylintrc b/src/brokkr/.pylintrc index 1587017..60da777 100644 --- a/src/brokkr/.pylintrc +++ b/src/brokkr/.pylintrc @@ -437,7 +437,7 @@ valid-metaclass-classmethod-first-arg=cls [DESIGN] # Maximum number of arguments for function / method. -max-args=12 +max-args=15 # Maximum number of attributes for a class (see R0902). max-attributes=15 @@ -452,7 +452,7 @@ max-branches=12 max-locals=20 # Maximum number of parents for a class (see R0901). -max-parents=7 +max-parents=10 # Maximum number of public methods for a class (see R0904). max-public-methods=20 diff --git a/src/brokkr/inputs/adafruitadc.py b/src/brokkr/inputs/adafruitadc.py new file mode 100644 index 0000000..4203228 --- /dev/null +++ b/src/brokkr/inputs/adafruitadc.py @@ -0,0 +1,60 @@ +""" +Input steps for Adafruit digital I2C devices (e.g. SHT31, HTU21, BMP280, etc). +""" + +# Standard library imports +import importlib + +# Third party imports +import board +import busio + +# Local imports +import brokkr.pipeline.baseinput + + +DEFAULT_ADC_CHANNEL = 0 +DEFAULT_ADC_MODULE = "adafruit_ads1x15.ads1115" +DEFAULT_ADC_CLASS = "ADS1115" +DEFAULT_ANALOG_MODULE = "adafruit_ads1x15.analog_in" +DEFAULT_ANALOG_CLASS = "AnalogIn" + + +class AdafruitADCInput(brokkr.pipeline.baseinput.PropertyInputStep): + def __init__( + self, + sensor_module, + sensor_class, + adc_channel=None, + adc_kwargs=None, + analog_module=DEFAULT_ANALOG_MODULE, + analog_class=DEFAULT_ANALOG_CLASS, + i2c_kwargs=None, + **property_input_kwargs): + super().__init__( + sensor_module=sensor_module, + sensor_class=sensor_class, + cache_sensor_object=False, + **property_input_kwargs) + self._i2c_kwargs = {} if i2c_kwargs is None else i2c_kwargs + self._adc_kwargs = {} if adc_kwargs is None else adc_kwargs + + analog_object = importlib.import_module(analog_module) + self._analog_class = getattr(analog_object, analog_class) + + if (adc_channel is None + and self._adc_kwargs.get("positive_pin", None) is None): + adc_channel = DEFAULT_ADC_CHANNEL + if adc_channel is not None: + self._adc_kwargs["positive_pin"] = adc_channel + + def read_sensor_data(self, sensor_object=None): + with busio.I2C(board.SCL, board.SDA, **self._i2c_kwargs) as i2c: + sensor_object = self.init_sensor_object(i2c) + if sensor_object is None: + return None + channel_object = self._analog_class( + sensor_object, **self._adc_kwargs) + raw_data = super().read_sensor_data( + sensor_object=channel_object) + return raw_data diff --git a/src/brokkr/inputs/adafruiti2c.py b/src/brokkr/inputs/adafruiti2c.py new file mode 100644 index 0000000..05f6242 --- /dev/null +++ b/src/brokkr/inputs/adafruiti2c.py @@ -0,0 +1,42 @@ +""" +Input steps for Adafruit digital I2C devices (e.g. SHT31, HTU21, BMP280, etc). +""" + +# Third party imports +import board +import busio + +# Local imports +import brokkr.pipeline.baseinput + + +class BaseAdafruitI2CInput(brokkr.pipeline.baseinput.PropertyInputStep): + def __init__( + self, + i2c_kwargs=None, + **property_input_kwargs): + super().__init__(**property_input_kwargs) + self._i2c_kwargs = {} if i2c_kwargs is None else i2c_kwargs + + +class AdafruitI2CInput(BaseAdafruitI2CInput): + def read_sensor_data(self, sensor_object=None): + with busio.I2C(board.SCL, board.SDA, **self._i2c_kwargs) as i2c: + sensor_object = self.init_sensor_object(i2c) + if sensor_object is None: + return None + sensor_data = super().read_sensor_data(sensor_object=sensor_object) + return sensor_data + + +class AdafruitPersistantI2CInput(BaseAdafruitI2CInput): + def __init__( + self, + **adafruit_i2c_input_kwargs): + super().__init__(cache_sensor_object=True, **adafruit_i2c_input_kwargs) + + def init_sensor_object(self, *sensor_args, **sensor_kwargs): + i2c = busio.I2C(board.SCL, board.SDA, **self._i2c_kwargs) + sensor_object = super().init_sensor_object( + i2c, *sensor_args, **sensor_kwargs) + return sensor_object diff --git a/src/brokkr/inputs/adafruitonewire.py b/src/brokkr/inputs/adafruitonewire.py new file mode 100644 index 0000000..8a6a58a --- /dev/null +++ b/src/brokkr/inputs/adafruitonewire.py @@ -0,0 +1,18 @@ +""" +Input steps for Adafruit Onewire devices (e.g. DHT11, DHT22). +""" + +# Local imports +import brokkr.pipeline.baseinput + + +class AdafruitOnewireInput(brokkr.pipeline.baseinput.PropertyInputStep): + def __init__(self, pin, sensor_kwargs=None, **property_input_kwargs): + if sensor_kwargs is None: + sensor_kwargs = {} + sensor_kwargs["pin"] = pin + + super().__init__( + sensor_kwargs=sensor_kwargs, + cache_sensor_object=True, + **property_input_kwargs) diff --git a/src/brokkr/inputs/gpiocounter.py b/src/brokkr/inputs/gpiocounter.py new file mode 100644 index 0000000..b806f5f --- /dev/null +++ b/src/brokkr/inputs/gpiocounter.py @@ -0,0 +1,89 @@ +""" +Input steps for digital GPIO devices. +""" + +# Standard library imports +import collections +import time + +# Third party imports +import gpiozero + +# Local imports +import brokkr.pipeline.baseinput +import brokkr.utils.misc + + +class GPIOCounterDevice(brokkr.utils.misc.AutoReprMixin): + def __init__(self, pin, max_counts=None, gpio_kwargs=None): + self._gpio_kwargs = {} if gpio_kwargs is None else gpio_kwargs + self._gpio_kwargs["pin"] = pin + + self._count_times = collections.deque(maxlen=max_counts) + self._gpio_device = gpiozero.DigitalInputDevice(**gpio_kwargs) + self._gpio_device.when_activated = self._count + + self.start_time = time.monotonic() + + def _count(self): + """Count one transition. Used as a callback.""" + self._count_times.append(time.monotonic()) + + @property + def time_elapsed_s(self): + """The time elapsed, in s, since the start time was last reset.""" + return time.monotonic() - self.start_time + + def get_count(self, period_s=None, mean=False): + if not period_s: + count = len(self._count_times) + else: + # Tabulate the number of counts over a given period + count = -1 + for count, count_time in enumerate(reversed(self._count_times)): + if count_time < (time.monotonic() - period_s): + break + else: + count += 1 + + if mean: + count = count / max([min([self.time_elapsed_s, period_s]), + time.get_clock_info("time").resolution]) + + return count + + def reset(self): + """ + Reset the count to zero and start time to the current time. + + Returns + ------- + None. + + """ + self._count_times.clear() + self.start_time = time.monotonic() + + +class GPIOCounterInput(brokkr.pipeline.baseinput.ValueInputStep): + def __init__( + self, + pin, + max_counts=None, + gpio_kwargs=None, + reset_after_read=False, + **value_input_kwargs): + super().__init__(**value_input_kwargs) + self._counter_device = GPIOCounterDevice( + pin=pin, max_counts=max_counts, gpio_kwargs=gpio_kwargs) + self._reset_after_read = reset_after_read + + def read_raw_data(self, input_data=None): + raw_data = [ + self._counter_device.get_count( + period_s=getattr(data_type, "period_s", None), + mean=getattr(data_type, "mean", False), + ) for data_type in self.data_types] + if self._reset_after_read: + self._counter_device.reset() + return raw_data diff --git a/src/brokkr/inputs/smbusi2c.py b/src/brokkr/inputs/smbusi2c.py new file mode 100644 index 0000000..e462386 --- /dev/null +++ b/src/brokkr/inputs/smbusi2c.py @@ -0,0 +1,110 @@ +""" +Generalized input class for an I2C/SMBus device using the SMBus library. +""" + +# Standard library imports +import logging +from pathlib import Path + +# Third party imports +import smbus2 + +# Local imports +import brokkr.pipeline.baseinput +import brokkr.utils.misc + + +MAX_I2C_BUS_N = 6 + +I2C_BLOCK_READ_FUNCTION = "read_i2c_block_data" +DEFAULT_READ_FUNCTION = I2C_BLOCK_READ_FUNCTION + +LOGGER = logging.getLogger(__name__) + + +class SMBusI2CDevice(brokkr.utils.misc.AutoReprMixin): + def __init__(self, bus=None, force=None): + self.force = force + + # Automatically try to find first I2C bus and use that + if bus is None: + for n_bus in range(0, MAX_I2C_BUS_N + 1): + if Path(f"/dev/i2c-{n_bus}").exists(): + bus = n_bus + LOGGER.debug("Found I2C device at bus %s", bus) + break + else: + raise RuntimeError("Could not find I2C any bus device") + + self.bus = bus + + def read(self, force=None, + read_function=DEFAULT_READ_FUNCTION, **read_kwargs): + if force is None: + force = self.force + LOGGER.debug("Reading I2C data with function %s at bus %r, kwargs %r", + read_function, self.bus, read_kwargs) + with smbus2.SMBus(self.bus, force=self.force) as i2c_bus: + buffer = getattr(i2c_bus, read_function)( + force=force, **read_kwargs) + LOGGER.debug("Read I2C data %r", buffer) + return buffer + + +class SMBusI2CInput(brokkr.pipeline.baseinput.SensorInputStep): + def __init__( + self, + bus=None, + read_function=DEFAULT_READ_FUNCTION, + init_kwargs=None, + read_kwargs=None, + include_all_data_each=True, + **sensor_input_kwargs): + init_kwargs = {} if init_kwargs is None else init_kwargs + self._read_kwargs = {} if read_kwargs is None else read_kwargs + self._read_function = read_function + sensor_kwargs = {"bus": bus, **init_kwargs} + + super().__init__( + sensor_class=SMBusI2CDevice, + sensor_kwargs=sensor_kwargs, + include_all_data_each=include_all_data_each, + **sensor_input_kwargs) + + def read_sensor_data(self, sensor_object=None): + sensor_object = self.get_sensor_object(sensor_object=sensor_object) + if sensor_object is None: + return None + + try: + sensor_data = sensor_object.read( + read_function=self._read_function, **self._read_kwargs) + except Exception as e: + self.logger.error( + "%s reading data from I2C SMBus device with function %s " + "of %s sensor object %s on step %s: %s", + type(e).__name__, self._read_function, + type(self), self.object_class, self.name, e) + self.logger.info("Error details:", exc_info=True) + sensor_data = None + + return sensor_data + + +class SMBusI2CBlockInput(SMBusI2CInput): + def __init__( + self, + i2c_addr, + register=0, + length=1, + force=None, + **smbus_input_kwargs): + read_kwargs = { + "i2c_addr": i2c_addr, + "register": register, + "length": length, + "force": force, + } + + super().__init__(read_function=I2C_BLOCK_READ_FUNCTION, + read_kwargs=read_kwargs, **smbus_input_kwargs) diff --git a/src/brokkr/outputs/print.py b/src/brokkr/outputs/print.py index f66e8e1..b5a0d4d 100644 --- a/src/brokkr/outputs/print.py +++ b/src/brokkr/outputs/print.py @@ -41,7 +41,7 @@ def __init__(self, in_place=True, **print_output_kwargs): super().__init__(in_place=in_place, **print_output_kwargs) def execute(self, input_data=None): - output_data = brokkr.utils.output.format_data(input_data) + output_data = brokkr.utils.output.format_data(input_data) + "\n" if self.in_place and self.ran_once: output_data = ( (CURSOR_UP_CHAR + ERASE_LINE_CHAR) * output_data.count("\n") diff --git a/src/brokkr/pipeline/baseinput.py b/src/brokkr/pipeline/baseinput.py index 0b4365a..7eedb99 100644 --- a/src/brokkr/pipeline/baseinput.py +++ b/src/brokkr/pipeline/baseinput.py @@ -4,6 +4,7 @@ # Standard library imports import abc +import importlib # Local imports import brokkr.pipeline.base @@ -11,6 +12,8 @@ import brokkr.pipeline.datavalue +# --- Core base classes --- # + class ValueInputStep(brokkr.pipeline.base.InputStep, metaclass=abc.ABCMeta): def __init__( self, @@ -19,6 +22,7 @@ def __init__( datatype_default_kwargs=None, conversion_functions=None, na_marker=None, + include_all_data_each=False, **pipeline_step_kwargs): super().__init__(**pipeline_step_kwargs) if datatype_default_kwargs is None: @@ -31,7 +35,7 @@ def __init__( except AttributeError: # If data_type isn't already an object try: data_type["name"] - except TypeError: + except TypeError: # If data_types is a list, not a dict data_type_dict = data_types[data_type] data_type_dict["name"] = data_type data_type = data_type_dict @@ -49,6 +53,7 @@ def __init__( data_types=self.data_types, conversion_functions=conversion_functions, na_marker=na_marker, + include_all_data_each=include_all_data_each, ) @abc.abstractmethod @@ -56,7 +61,7 @@ def read_raw_data(self, input_data=None): pass def decode_data(self, raw_data): - self.logger.debug("Created data decoder: %r", self.decoder) + # self.logger.debug("Created data decoder: %r", self.decoder) decoded_data = self.decoder.decode_data(raw_data) return decoded_data @@ -65,3 +70,121 @@ def execute(self, input_data=None): raw_data = self.read_raw_data(input_data=input_data) output_data = self.decode_data(raw_data) return output_data + + +class SensorInputStep(ValueInputStep, metaclass=abc.ABCMeta): + def __init__( + self, + sensor_class, + sensor_module=None, + sensor_args=None, + sensor_kwargs=None, + cache_sensor_object=False, + binary_decoder=False, + **value_input_kwargs): + super().__init__(binary_decoder=binary_decoder, **value_input_kwargs) + self.sensor_args = () if sensor_args is None else sensor_args + self.sensor_kwargs = {} if sensor_kwargs is None else sensor_kwargs + self.sensor_object = None + self.cache_sensor_object = cache_sensor_object + + if sensor_module is not None: + module_object = importlib.import_module(sensor_module) + sensor_class = getattr(module_object, sensor_class) + self.object_class = sensor_class + + def init_sensor_object(self, *sensor_args, **sensor_kwargs): + if not sensor_args: + sensor_args = self.sensor_args + if not sensor_kwargs: + sensor_kwargs = self.sensor_kwargs + + self.logger.debug( + "Initializing sensor object %s with args %r, kwargs %s", + self.object_class, sensor_args, sensor_kwargs) + try: + sensor_object = self.object_class( + *sensor_args, **sensor_kwargs) + except Exception as e: + self.logger.error( + "%s initializing %s sensor object %s on step %s: %s", + type(e).__name__, type(self), self.object_class, + self.name, e) + self.logger.info("Error details:", exc_info=True) + self.logger.info("Sensor args: %r | Sensor kwargs: %r:", + sensor_args, sensor_kwargs) + sensor_object = None + self.logger.debug( + "Initialized sensor object %s to %r", + self.object_class, sensor_object) + + if self.cache_sensor_object: + self.sensor_object = sensor_object + self.logger.debug("Cached sensor object %s", self.object_class) + return sensor_object + + def get_sensor_object(self, sensor_object=None): + if sensor_object is not None: + return sensor_object + if self.sensor_object is not None: + return self.sensor_object + + sensor_object = self.init_sensor_object() + return sensor_object + + @abc.abstractmethod + def read_sensor_data(self, sensor_object=None): + pass + + def read_raw_data(self, input_data=None): + raw_data = self.read_sensor_data() + return raw_data + + +class AttributeInputStep(SensorInputStep, metaclass=abc.ABCMeta): + @abc.abstractmethod + def read_sensor_value(self, sensor_object, data_type): + pass + + def read_sensor_data(self, sensor_object=None): + sensor_object = self.get_sensor_object(sensor_object=sensor_object) + if sensor_object is None: + return None + + sensor_data = [] + for data_type in self.data_types: + try: + data_value = self.read_sensor_value( + sensor_object=sensor_object, data_type=data_type) + except Exception as e: + self.logger.error( + "%s on attribute %s from %s sensor object %s " + "on step %s: %s", + type(e).__name__, data_type.attribute_name, type(self), + self.object_class, self.name, e) + self.logger.info("Error details:", exc_info=True) + data_value = None + else: + self.logger.debug( + "Read value %r from attribute %s of sensor %s", + data_value, data_type.attribute_name, self.object_class) + sensor_data.append(data_value) + return sensor_data + + +class PropertyInputStep(AttributeInputStep): + def read_sensor_value(self, sensor_object, data_type): + if sensor_object is None: + sensor_object = self.sensor_object + data_value = getattr(sensor_object, data_type.attribute_name) + return data_value + + +class MethodInputStep(AttributeInputStep): + def read_sensor_value(self, sensor_object, data_type): + if sensor_object is None: + sensor_object = self.sensor_object + function_kwargs = getattr(data_type, "function_kwargs", {}) + data_value = getattr(sensor_object, data_type.attribute_name)( + **function_kwargs) + return data_value diff --git a/src/brokkr/pipeline/datavalue.py b/src/brokkr/pipeline/datavalue.py index cb516f9..f7d8e69 100644 --- a/src/brokkr/pipeline/datavalue.py +++ b/src/brokkr/pipeline/datavalue.py @@ -13,6 +13,7 @@ def __init__( conversion=True, binary_type=None, input_type=None, + digits=None, na_marker=None, full_name=None, unit=None, @@ -20,12 +21,12 @@ def __init__( range_min=None, range_max=None, custom_attrs=None, - **conversion_kwargs, - ): + **conversion_kwargs): self.name = name self.conversion = conversion self.binary_type = binary_type self.input_type = binary_type if input_type is None else input_type + self.digits = digits self.na_marker = na_marker self.conversion_kwargs = conversion_kwargs diff --git a/src/brokkr/pipeline/decode.py b/src/brokkr/pipeline/decode.py index d0e5386..5103b77 100644 --- a/src/brokkr/pipeline/decode.py +++ b/src/brokkr/pipeline/decode.py @@ -3,19 +3,35 @@ """ # Standard library imports +import ast import datetime import logging +import math +import operator import struct +# Third party imports +import simpleeval + # Local imports import brokkr.pipeline.datavalue import brokkr.utils.misc +import brokkr.utils.output NA_MARKER_DEFAULT = "NA" OUTPUT_CUSTOM = "custom" +EVAL_OPERATORS_EXTRA = { + ast.BitAnd: operator.and_, + ast.BitOr: operator.or_, + ast.BitXor: operator.xor, + ast.Invert: operator.invert, + ast.LShift: operator.lshift, + ast.RShift: operator.rshift, + } + def _convert_none(value): # pylint: disable=unused-argument @@ -30,6 +46,10 @@ def _convert_bitfield(value): return int(value) +def _convert_bool(value): + return bool(value) + + def _convert_byte(value): return int(value) @@ -46,8 +66,8 @@ def _convert_float(value): return float(value) -def _convert_int(value): - return int(value) +def _convert_int(value, **kwargs): + return int(value, **kwargs) def _convert_str(value): @@ -77,10 +97,23 @@ def _convert_timestamp(value, time_format="%Y-%m-%d %H:%M:%S"): return datetime.datetime.strptime(value, time_format) +def _convert_custom(value, base=2, power=0, scale=1, offset=0): + return value * (base ** power) * scale + offset + + +def _convert_eval(value, expression): + value_parser = simpleeval.SimpleEval(names={"value": value}) + value_parser.operators = { + **value_parser.operators, **EVAL_OPERATORS_EXTRA} + value = value_parser.eval(expression) + return value + + CONVERSION_FUNCTIONS = { False: _convert_none, True: _convert_pass, "bitfield": _convert_bitfield, + "bool": _convert_bool, "byte": _convert_byte, "bytestr": _convert_bytestr, "bytestr_strip": _convert_bytestr_strip, @@ -90,21 +123,36 @@ def _convert_timestamp(value, time_format="%Y-%m-%d %H:%M:%S"): "time_posix": _convert_time_posix, "time_posix_ms": _convert_time_posix_ms, "timestamp": _convert_timestamp, + "custom": _convert_custom, + "eval": _convert_eval, } -def convert_custom( - value, scale=1, offset=0, base=2, power=0, digits=None, after=None, - **after_kwargs): - value = value * (base ** power) * scale + offset - if digits is not None: - value = round(value, digits) +def convert_multistep( + value, + before=None, + before_kwargs=None, + main=None, + after=None, + after_kwargs=None, + **main_kwargs, + ): + if before_kwargs is None: + before_kwargs = {} + if after_kwargs is None: + after_kwargs = {} + + if before is not None: + value = CONVERSION_FUNCTIONS[before](value, **before_kwargs) + if main is not None: + value = CONVERSION_FUNCTIONS[main](value, **main_kwargs) if after is not None: value = CONVERSION_FUNCTIONS[after](value, **after_kwargs) + return value -CONVERSION_FUNCTIONS["custom"] = convert_custom +CONVERSION_FUNCTIONS["multistep"] = convert_multistep LOGGER = logging.getLogger(__name__) @@ -118,8 +166,10 @@ def __init__( data_types, na_marker=None, conversion_functions=None, + include_all_data_each=False, ): self.data_types = data_types + self.include_all_data_each = include_all_data_each if conversion_functions is None: conversion_functions = {} conversion_functions = { @@ -145,27 +195,28 @@ def output_na_values(self): return output_data def convert_data(self, raw_data): - if not raw_data: - output_data = self.output_na_values() - LOGGER.debug("No data to convert, returning: %r", output_data) - return output_data - error_count = 0 output_data = {} - for data_type, value in zip(self.data_types, raw_data): + for idx, data_type in enumerate(self.data_types): if not data_type.conversion: continue # If this data value should be dropped, ignore it + value = raw_data + if not self.include_all_data_each: + value = value[idx] if value is None: - LOGGER.debug("Data value is None decoding data_type %r to %s, " - "coercing to NA", - data_type.name, data_type.conversion) + LOGGER.debug("Data value is None decoding data_type %s to %s, " + "coercing to NA value %r", + data_type.name, data_type.conversion, + self.output_na_value(data_type)) output_data[data_type.name] = self.output_na_value(data_type) continue try: output_value = ( self.conversion_functions[data_type.conversion]( value, **data_type.conversion_kwargs)) + if data_type.digits is not None: + output_value = round(output_value, data_type.digits) # Handle errors decoding specific values except Exception as e: if error_count < 1: @@ -190,6 +241,9 @@ def convert_data(self, raw_data): 1, **data_type.conversion_kwargs) - self.conversion_functions[data_type.conversion]( 0, **data_type.conversion_kwargs)) + uncertainty = round( + uncertainty, -int(math.floor(math.log10(uncertainty)))) + else: uncertainty = data_type.uncertainty data_value = brokkr.pipeline.datavalue.DataValue( @@ -198,16 +252,20 @@ def convert_data(self, raw_data): output_data[data_type.name] = data_value if error_count > 1: - LOGGER.warning("%s additioanl decode errors were suppressed.", + LOGGER.warning("%s additional decode errors were suppressed.", error_count - 1) - LOGGER.debug("Converted data: %r", output_data) + LOGGER.debug("Converted data: {%s}", brokkr.utils.output.format_data( + data=output_data, seperator=", ", include_raw=True)) return output_data def decode_data(self, data): if data is None: - LOGGER.debug("No data to decode") output_data = self.output_na_values() + LOGGER.debug( + "No data to decode, returning NAs: %r", + brokkr.utils.output.format_data( + data=output_data, seperator=", ", include_raw=False)) else: output_data = self.convert_data(data) return output_data diff --git a/src/brokkr/utils/output.py b/src/brokkr/utils/output.py index 548db51..85ce6a1 100644 --- a/src/brokkr/utils/output.py +++ b/src/brokkr/utils/output.py @@ -49,7 +49,7 @@ def format_data(data=None, seperator="\n", include_raw=False): data_item = " ".join(data_componets) output_data_list.append(data_item) - formatted_data = seperator.join(output_data_list) + "\n" + formatted_data = seperator.join(output_data_list) return formatted_data