Skip to content

Commit 749cb9a

Browse files
github-actions[bot]jsimonetti
authored andcommitted
Auto-generate documentation from Pydantic models
- Updated SETTINGS.md - Updated config_schema.json Generated by GitHub Actions workflow Signed-off-by: Jeroen Simonetti <jeroen@simonetti.nl>
1 parent 18b6b67 commit 749cb9a

File tree

17 files changed

+837
-276
lines changed

17 files changed

+837
-276
lines changed

AGENTS.md

Lines changed: 440 additions & 0 deletions
Large diffs are not rendered by default.

SETTINGS.md

Lines changed: 72 additions & 77 deletions
Large diffs are not rendered by default.

config_schema.json

Lines changed: 29 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@
371371
"type": "string"
372372
}
373373
],
374+
"default": true,
374375
"description": "Whether boiler is present/enabled",
375376
"title": "Boiler Present"
376377
},
@@ -416,6 +417,7 @@
416417
"title": "Entity Instant Start"
417418
},
418419
"cop": {
420+
"default": 3.0,
419421
"description": "Coefficient of Performance",
420422
"exclusiveMinimum": 0.0,
421423
"title": "Cop",
@@ -428,6 +430,7 @@
428430
"type": "number"
429431
},
430432
"volume": {
433+
"default": 200.0,
431434
"description": "Water volume in liters",
432435
"exclusiveMinimum": 0.0,
433436
"title": "Volume",
@@ -439,6 +442,7 @@
439442
"type": "number"
440443
},
441444
"elec. power": {
445+
"default": 1000.0,
442446
"description": "Electrical power in watts",
443447
"exclusiveMinimum": 0.0,
444448
"title": "Elec. Power",
@@ -456,15 +460,11 @@
456460
}
457461
},
458462
"required": [
459-
"boiler present",
460463
"entity actual temp.",
461464
"entity setpoint",
462465
"entity hysterese",
463-
"cop",
464466
"cooling rate",
465-
"volume",
466467
"heating allowed below",
467-
"elec. power",
468468
"activate service",
469469
"activate entity"
470470
],
@@ -676,11 +676,6 @@
676676
"title": "Entity Position",
677677
"type": "string"
678678
},
679-
"entity max amperage": {
680-
"description": "HA entity for maximum charging amperage",
681-
"title": "Entity Max Amperage",
682-
"type": "string"
683-
},
684679
"charge three phase": {
685680
"anyOf": [
686681
{
@@ -690,6 +685,7 @@
690685
"type": "string"
691686
}
692687
],
688+
"default": true,
693689
"description": "Whether vehicle charges on three phases",
694690
"title": "Charge Three Phase"
695691
},
@@ -770,8 +766,6 @@
770766
"name",
771767
"capacity",
772768
"entity position",
773-
"entity max amperage",
774-
"charge three phase",
775769
"charge stages",
776770
"entity actual level",
777771
"entity plugged in",
@@ -1011,21 +1005,32 @@
10111005
"type": "string"
10121006
}
10131007
],
1008+
"default": false,
10141009
"description": "Whether heating system is present/enabled",
10151010
"title": "Heater Present"
10161011
},
10171012
"entity hp enabled": {
1013+
"anyOf": [
1014+
{
1015+
"type": "string"
1016+
},
1017+
{
1018+
"type": "null"
1019+
}
1020+
],
1021+
"default": null,
10181022
"description": "HA binary sensor for heat pump enabled status",
1019-
"title": "Entity Hp Enabled",
1020-
"type": "string"
1023+
"title": "Entity Hp Enabled"
10211024
},
10221025
"degree days factor": {
1026+
"default": 1.0,
10231027
"description": "Degree days factor for heat demand calculation",
10241028
"exclusiveMinimum": 0.0,
10251029
"title": "Degree Days Factor",
10261030
"type": "number"
10271031
},
10281032
"adjustment": {
1033+
"default": "power",
10291034
"description": "Adjustment mode",
10301035
"enum": [
10311036
"on/off",
@@ -1071,18 +1076,11 @@
10711076
"title": "Adjustment Factor"
10721077
},
10731078
"min run length": {
1074-
"anyOf": [
1075-
{
1076-
"minimum": 1,
1077-
"type": "integer"
1078-
},
1079-
{
1080-
"type": "null"
1081-
}
1082-
],
1083-
"default": null,
1079+
"default": 1,
10841080
"description": "Minimum run length in time intervals",
1085-
"title": "Min Run Length"
1081+
"minimum": 1,
1082+
"title": "Min Run Length",
1083+
"type": "integer"
10861084
},
10871085
"entity hp heat produced": {
10881086
"anyOf": [
@@ -1164,10 +1162,6 @@
11641162
}
11651163
},
11661164
"required": [
1167-
"heater present",
1168-
"entity hp enabled",
1169-
"degree days factor",
1170-
"adjustment",
11711165
"stages"
11721166
],
11731167
"title": "HeatingConfig",
@@ -1242,20 +1236,7 @@
12421236
"description": "Home Assistant port (default: 8123)",
12431237
"title": "Ip Port"
12441238
},
1245-
"ssl": {
1246-
"anyOf": [
1247-
{
1248-
"type": "boolean"
1249-
},
1250-
{
1251-
"type": "null"
1252-
}
1253-
],
1254-
"default": null,
1255-
"description": "Whether to use SSL/HTTPS",
1256-
"title": "Ssl"
1257-
},
1258-
"hasstoken": {
1239+
"token": {
12591240
"anyOf": [
12601241
{
12611242
"type": "string"
@@ -1269,7 +1250,7 @@
12691250
],
12701251
"default": null,
12711252
"description": "Home Assistant long-lived access token (can use !secret)",
1272-
"title": "Hasstoken"
1253+
"title": "Token"
12731254
},
12741255
"protocol api": {
12751256
"anyOf": [
@@ -1451,6 +1432,7 @@
14511432
"description": "Day-ahead pricing and tariff configuration.",
14521433
"properties": {
14531434
"source day ahead": {
1435+
"default": "nordpool",
14541436
"description": "Source for day-ahead prices",
14551437
"enum": [
14561438
"nordpool",
@@ -1476,25 +1458,6 @@
14761458
"description": "ENTSO-E API key (can use !secret)",
14771459
"title": "Entsoe-Api-Key"
14781460
},
1479-
"regular high": {
1480-
"description": "Regular high tariff fallback (euro/kWh)",
1481-
"minimum": 0.0,
1482-
"title": "Regular High",
1483-
"type": "number"
1484-
},
1485-
"regular low": {
1486-
"description": "Regular low tariff fallback (euro/kWh)",
1487-
"minimum": 0.0,
1488-
"title": "Regular Low",
1489-
"type": "number"
1490-
},
1491-
"switch to low": {
1492-
"description": "Hour to switch to low tariff",
1493-
"maximum": 23,
1494-
"minimum": 0,
1495-
"title": "Switch To Low",
1496-
"type": "integer"
1497-
},
14981461
"energy taxes consumption": {
14991462
"additionalProperties": {
15001463
"type": "number"
@@ -1562,10 +1525,6 @@
15621525
}
15631526
},
15641527
"required": [
1565-
"source day ahead",
1566-
"regular high",
1567-
"regular low",
1568-
"switch to low",
15691528
"energy taxes consumption",
15701529
"energy taxes production",
15711530
"cost supplier consumption",
@@ -1898,8 +1857,8 @@
18981857
"description": "Configuration schema for the Day Ahead Optimizer Home Assistant add-on. This schema defines all available configuration options including battery settings, solar panels, pricing, scheduler, graphics, and more.",
18991858
"properties": {
19001859
"config_version": {
1860+
"const": 0,
19011861
"default": 0,
1902-
"description": "Configuration version number",
19031862
"title": "Config Version",
19041863
"type": "integer"
19051864
},
@@ -1990,12 +1949,8 @@
19901949
},
19911950
{
19921951
"$ref": "#/$defs/SecretStr"
1993-
},
1994-
{
1995-
"type": "null"
19961952
}
19971953
],
1998-
"default": null,
19991954
"description": "Meteoserver API key (can use !secret)",
20001955
"title": "Meteoserver-Key"
20011956
},
@@ -2240,6 +2195,9 @@
22402195
"description": "Task scheduler configuration"
22412196
}
22422197
},
2198+
"required": [
2199+
"meteoserver-key"
2200+
],
22432201
"title": "Day Ahead Optimizer Configuration",
22442202
"type": "object",
22452203
"$schema": "http://json-schema.org/draft-07/schema#"

dao/prog/config/generate_docs.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -58,38 +58,30 @@ def generate_markdown_from_schema(schema: dict[str, Any], title_prefix: str = ""
5858
# Extract field info from JSON schema
5959
field_type = get_type_from_schema(field_schema, schema.get('$defs', {}))
6060
is_required = field_name in required
61-
description = field_schema.get('description', field_schema.get('title', ''))
61+
description = field_schema.get('description', '')
6262

63-
# Validate description exists
63+
# Validate description exists (use title as fallback for SecretStr/FlexValue internal fields)
6464
if not description:
65-
validation_errors.append(f"ERROR: {model_title}.{field_name} - Missing description")
66-
description = "MISSING DESCRIPTION"
65+
title = field_schema.get('title', '')
66+
if title:
67+
# For internal model fields like SecretStr.secret_key, use title
68+
description = title
69+
else:
70+
validation_errors.append(f"ERROR: {model_title}.{field_name} - Missing description")
71+
description = "MISSING DESCRIPTION"
6772

6873
# Append enum options to description if present
6974
if 'enum' in field_schema:
7075
enum_values = field_schema['enum']
71-
enum_str = ' | '.join(f'`{v}`' for v in enum_values)
76+
enum_str = ', '.join(f'`{v}`' for v in enum_values)
7277
description = f"{description}. Options: {enum_str}"
7378

7479
# Format required column
7580
required_mark = "Yes" if is_required else "No"
7681

77-
# Check if this is a pure model reference (starts with [ and no pipe or 'optional')
78-
# Pure model refs: [BatteryConfig](#...)
79-
# Union types: string | [SecretStr](#...) (optional)
80-
is_pure_model_ref = (
81-
field_type.startswith('[') and
82-
'|' not in field_type and
83-
not field_type.endswith('(optional)')
84-
)
85-
86-
if is_pure_model_ref:
87-
# Empty default column for pure model references
88-
lines.append(f"| `{field_name}` | {field_type} | {required_mark} | | {description} |")
89-
else:
90-
# Include default value for primitive types and union types
91-
default = get_default_from_schema(field_schema, is_required, schema.get('$defs', {}))
92-
lines.append(f"| `{field_name}` | {field_type} | {required_mark} | {default} | {description} |")
82+
# Get default value for all field types
83+
default = get_default_from_schema(field_schema, is_required, schema.get('$defs', {}))
84+
lines.append(f"| `{field_name}` | {field_type} | {required_mark} | {default} | {description} |")
9385

9486
lines.append("")
9587
return "\n".join(lines), validation_errors
@@ -103,7 +95,7 @@ def get_type_from_schema(field_schema: dict[str, Any], defs: dict[str, Any]) ->
10395
anchor = ref_path.lower()
10496
return f"[{ref_path}](#{anchor})"
10597

106-
# Handle anyOf (used for Optional types)
98+
# Handle anyOf (used for Optional types and unions)
10799
if 'anyOf' in field_schema:
108100
types = []
109101
has_null = False
@@ -112,7 +104,11 @@ def get_type_from_schema(field_schema: dict[str, Any], defs: dict[str, Any]) ->
112104
has_null = True
113105
else:
114106
types.append(get_type_from_schema(sub_schema, defs))
115-
type_str = " | ".join(types) if types else "unknown"
107+
# Use 'or' for readability instead of pipe which breaks markdown tables
108+
if len(types) > 1:
109+
type_str = " or ".join(types)
110+
else:
111+
type_str = types[0] if types else "unknown"
116112
return f"{type_str} (optional)" if has_null else type_str
117113

118114
# Handle arrays
@@ -141,32 +137,25 @@ def get_default_from_schema(field_schema: dict[str, Any], is_required: bool, def
141137

142138
# Check if default is specified
143139
if 'default' not in field_schema:
144-
# Check if it's a reference to another model
140+
# Model references (not Optional) → uses default_factory, can omit but NOT set to null
145141
if '$ref' in field_schema:
146-
ref_path = field_schema['$ref'].split('/')[-1]
147-
# Create anchor link to the model's section (lowercase, no special chars)
148-
anchor = ref_path.lower()
149-
return f"[No default](#{anchor})"
142+
return "_See nested fields_"
150143

151-
# Check anyOf for references
144+
# Union types with model refs → may have default_factory
152145
if 'anyOf' in field_schema:
153146
for sub_schema in field_schema['anyOf']:
154147
if '$ref' in sub_schema:
155-
ref_path = sub_schema['$ref'].split('/')[-1]
156-
anchor = ref_path.lower()
157-
return f"[No default](#{anchor})"
148+
# If it's Optional[ModelType], can be null
149+
if any(s.get('type') == 'null' for s in field_schema['anyOf']):
150+
return "`null`"
151+
# Otherwise, has default_factory
152+
return "_See nested fields_"
158153

159-
return "[No default](#optional-vs-required-fields)"
154+
# Simple types without default → defaults to None/null
155+
return "`null`"
160156

161157
default = field_schema['default']
162158

163-
# For union types that include a model reference (like str | SecretStr),
164-
# if default is null, don't show it - treat as no default
165-
if default is None and 'anyOf' in field_schema:
166-
for sub_schema in field_schema['anyOf']:
167-
if '$ref' in sub_schema:
168-
return "" # Empty default for union types with model refs
169-
170159
if default is None:
171160
return "`null`"
172161
if isinstance(default, str):
@@ -243,10 +232,10 @@ def main():
243232
lines.append("")
244233
lines.append("| Default Value | Meaning |")
245234
lines.append("|---------------|---------|")
246-
lines.append("| `null` | Optional field, defaults to null/none |")
247-
lines.append("| `\"value\"`, `123`, `true`, etc. | Optional field with default value |")
248-
lines.append("| `[]`, `{{}}` | Optional field, empty by default |")
249-
lines.append("| [No default](#optional-vs-required-fields) | Optional, no default set |")
235+
lines.append("| `null` | Optional field, defaults to null/none (not set) |")
236+
lines.append("| `\"value\"`, `123`, `true`, etc. | Optional field with this default value |")
237+
lines.append("| `[]`, `{{}}` | Optional field, empty collection by default |")
238+
lines.append("| _See nested fields_ | Uses defaults from nested model (cannot be set to `null`) |")
250239
lines.append("| `—` | **Required** field - must be provided |")
251240
lines.append("")
252241

0 commit comments

Comments
 (0)