1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
|
"""Generate a markdown table that can be copied to home-assistant.io sensor.knx documentation."""
try:
from xknx.dpt import DPTBase
except ModuleNotFoundError:
exit(
"Add the `xknx` directory to python path via `export PYTHONPATH=$HOME/directory/to/xknx`"
)
# Defines the column order of the printed table.
# ["dpt_number", "value_type", "dpt_size", "unit", "dpt_range"]
COLUMN_ORDER = ["dpt_number", "value_type", "dpt_size", "dpt_range", "unit"]
# Defines the column adjustment of the printed table.
# "left", "right" or "center"
COLUMN_ADJUSTMENT = {
"value_type": "left",
"unit": "left",
"dpt_number": "right",
"dpt_size": "right",
"dpt_range": "center",
}
class Row:
"""A row in the table. Table header text is defined in __init__ defaults."""
column_width = {}
def __init__(
self,
value_type="type",
unit="unit",
dpt_number="KNX DPT",
dpt_size="size in byte",
dpt_range="range",
):
self.value_type = value_type
self._update_column_width("value_type", value_type)
self.unit = unit
self._update_column_width("unit", unit)
self.dpt_number = dpt_number
self._update_column_width("dpt_number", dpt_number)
self.dpt_size = dpt_size
self._update_column_width("dpt_size", dpt_size)
self.dpt_range = dpt_range
self._update_column_width("dpt_range", dpt_range)
def _update_column_width(self, index, text: str):
try:
Row.column_width[index] = max(Row.column_width[index], len(text))
except KeyError:
# index is not yet available
Row.column_width[index] = len(text)
def __repr__(self):
def _format_column_ljust(index):
content = getattr(self, index)
return f"| {content.ljust(Row.column_width[index] + 1)}"
_row = ""
for column in COLUMN_ORDER:
_row += _format_column_ljust(column)
_row += "|"
return _row
class DPTRow(Row):
"""A row holding information for a DPT."""
def __init__(self, dpt_class: DPTBase):
dpt_range = ""
if hasattr(dpt_class, "value_min") and hasattr(dpt_class, "value_max"):
dpt_range = f"{dpt_class.value_min} ... {dpt_class.value_max}"
dpt_number_str = self._get_dpt_number_from_docstring(dpt_class)
self.dpt_number_sort = self._dpt_number_sort(dpt_number_str)
dpt_number = self._dpt_number_str_repr(dpt_number_str)
super().__init__(
value_type=dpt_class.value_type,
unit=dpt_class.unit,
dpt_number=dpt_number,
dpt_size=str(dpt_class.payload_length),
dpt_range=dpt_range,
)
def _get_dpt_number_from_docstring(self, dpt_class: DPTBase):
"""Extract dpt number from class docstring."""
docstring = dpt_class.__doc__
try:
for line in docstring.splitlines():
text = line.strip()
if text.startswith("DPT"):
return text.split()[1]
except IndexError:
print(f"Error: Could not read docstring for: {dpt_class}")
print(f"Error: Could not find DPT in docstring for: {dpt_class}")
raise ValueError
def _dpt_number_sort(self, dpt_str: str) -> int:
"""Return dpt number as integer (for sorting). "xxx" is treated as 0."""
try:
dpt_major, dpt_minor = dpt_str.split(".")
if dpt_minor in ("x", "xxx", "*", "***"):
dpt_minor = -1
elif dpt_minor in ("?", "???"):
dpt_minor = 99999
return (int(dpt_major) * 100000) + int(dpt_minor)
except ValueError:
print(
f"Error: Could not parse dpt_number: '{self.dpt_number}' in '{self.value_type}'"
)
def _dpt_number_str_repr(self, dpt_str: str) -> str:
dpt_major, dpt_minor = dpt_str.split(".")
if dpt_minor in ("x", "xxx", "*", "***"):
return dpt_major
return dpt_str
def table_delimiter():
"""Build a row of table delimiters."""
def table_delimiter_ljust(width):
return "|-" + "-" * width + "-"
def table_delimiter_center(width):
return "|:" + "-" * width + ":"
def table_delimiter_rjust(width):
return "|-" + "-" * width + ":"
_row = ""
for column in COLUMN_ORDER:
_cell_width = Row.column_width[column]
if COLUMN_ADJUSTMENT[column] == "left":
_row += table_delimiter_ljust(_cell_width)
elif COLUMN_ADJUSTMENT[column] == "right":
_row += table_delimiter_rjust(_cell_width)
elif COLUMN_ADJUSTMENT[column] == "center":
_row += table_delimiter_center(_cell_width)
_row += "|"
return _row
def print_table():
"""Read the values and print the table to stdout."""
rows = []
for dpt in DPTBase.__recursive_subclasses__():
if dpt.has_distinct_value_type():
try:
row = DPTRow(dpt_class=dpt)
except ValueError:
continue
else:
rows.append(row)
rows.sort(key=lambda row: row.dpt_number_sort)
table_header = Row()
rows.insert(0, table_header)
# Insert at last to have correct column_widths.
rows.insert(1, table_delimiter())
for row in rows:
print(row)
if __name__ == "__main__":
print_table()
|