diff --git a/docs/_generated/config.md b/docs/_generated/config.md index a2e8f1b..c110988 100644 --- a/docs/_generated/config.md +++ b/docs/_generated/config.md @@ -21,11 +21,11 @@ Properties: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| version | `EOS_GENERAL__VERSION` | `str` | `rw` | `0.2.0+dev` | Configuration file version. Used to check compatibility. | -| data_folder_path | `EOS_GENERAL__DATA_FOLDER_PATH` | `Optional[pathlib.Path]` | `rw` | `None` | Path to EOS data directory. | -| data_output_subpath | `EOS_GENERAL__DATA_OUTPUT_SUBPATH` | `Optional[pathlib.Path]` | `rw` | `output` | Sub-path for the EOS output data directory. | -| latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°) | -| longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees, within -180 to 180 (°) | +| version | `EOS_GENERAL__VERSION` | `str` | `rw` | `0.2.0+dev` | - | +| data_folder_path | `EOS_GENERAL__DATA_FOLDER_PATH` | `Optional[pathlib.Path]` | `rw` | `None` | - | +| data_output_subpath | `EOS_GENERAL__DATA_OUTPUT_SUBPATH` | `Optional[pathlib.Path]` | `rw` | `output` | - | +| latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | - | +| longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | - | | timezone | | `Optional[str]` | `ro` | `N/A` | Compute timezone based on latitude and longitude. | | data_output_path | | `Optional[pathlib.Path]` | `ro` | `N/A` | Compute data_output_path based on data_folder_path. | | config_folder_path | | `Optional[pathlib.Path]` | `ro` | `N/A` | Path to EOS configuration directory. | @@ -76,8 +76,8 @@ Properties: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| subpath | `EOS_CACHE__SUBPATH` | `Optional[pathlib.Path]` | `rw` | `cache` | Sub-path for the EOS cache data directory. | -| cleanup_interval | `EOS_CACHE__CLEANUP_INTERVAL` | `float` | `rw` | `300` | Intervall in seconds for EOS file cache cleanup. | +| subpath | `EOS_CACHE__SUBPATH` | `Optional[pathlib.Path]` | `rw` | `cache` | - | +| cleanup_interval | `EOS_CACHE__CLEANUP_INTERVAL` | `float` | `rw` | `300` | - | ::: ### Example Input/Output @@ -101,9 +101,9 @@ Properties: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| startup_delay | `EOS_EMS__STARTUP_DELAY` | `float` | `rw` | `5` | Startup delay in seconds for EOS energy management runs. | -| interval | `EOS_EMS__INTERVAL` | `Optional[float]` | `rw` | `None` | Intervall in seconds between EOS energy management runs. | -| mode | `EOS_EMS__MODE` | `Optional[akkudoktoreos.core.emsettings.EnergyManagementMode]` | `rw` | `None` | Energy management mode [OPTIMIZATION | PREDICTION]. | +| startup_delay | `EOS_EMS__STARTUP_DELAY` | `float` | `rw` | `5` | - | +| interval | `EOS_EMS__INTERVAL` | `Optional[float]` | `rw` | `None` | - | +| mode | `EOS_EMS__MODE` | `Optional[akkudoktoreos.core.emsettings.EnergyManagementMode]` | `rw` | `None` | - | ::: ### Example Input/Output @@ -128,8 +128,8 @@ Properties: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| console_level | `EOS_LOGGING__CONSOLE_LEVEL` | `Optional[str]` | `rw` | `None` | Logging level when logging to console. | -| file_level | `EOS_LOGGING__FILE_LEVEL` | `Optional[str]` | `rw` | `None` | Logging level when logging to file. | +| console_level | `EOS_LOGGING__CONSOLE_LEVEL` | `Optional[str]` | `rw` | `None` | - | +| file_level | `EOS_LOGGING__FILE_LEVEL` | `Optional[str]` | `rw` | `None` | - | | file_path | | `Optional[pathlib.Path]` | `ro` | `N/A` | Computed log file path based on data output path. | ::: @@ -168,14 +168,14 @@ Properties: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| batteries | `EOS_DEVICES__BATTERIES` | `Optional[list[akkudoktoreos.devices.devices.BatteriesCommonSettings]]` | `rw` | `None` | List of battery devices | -| max_batteries | `EOS_DEVICES__MAX_BATTERIES` | `Optional[int]` | `rw` | `None` | Maximum number of batteries that can be set | -| electric_vehicles | `EOS_DEVICES__ELECTRIC_VEHICLES` | `Optional[list[akkudoktoreos.devices.devices.BatteriesCommonSettings]]` | `rw` | `None` | List of electric vehicle devices | -| max_electric_vehicles | `EOS_DEVICES__MAX_ELECTRIC_VEHICLES` | `Optional[int]` | `rw` | `None` | Maximum number of electric vehicles that can be set | -| inverters | `EOS_DEVICES__INVERTERS` | `Optional[list[akkudoktoreos.devices.devices.InverterCommonSettings]]` | `rw` | `None` | List of inverters | -| max_inverters | `EOS_DEVICES__MAX_INVERTERS` | `Optional[int]` | `rw` | `None` | Maximum number of inverters that can be set | -| home_appliances | `EOS_DEVICES__HOME_APPLIANCES` | `Optional[list[akkudoktoreos.devices.devices.HomeApplianceCommonSettings]]` | `rw` | `None` | List of home appliances | -| max_home_appliances | `EOS_DEVICES__MAX_HOME_APPLIANCES` | `Optional[int]` | `rw` | `None` | Maximum number of home_appliances that can be set | +| batteries | `EOS_DEVICES__BATTERIES` | `Optional[list[akkudoktoreos.devices.devices.BatteriesCommonSettings]]` | `rw` | `None` | - | +| max_batteries | `EOS_DEVICES__MAX_BATTERIES` | `Optional[int]` | `rw` | `None` | - | +| electric_vehicles | `EOS_DEVICES__ELECTRIC_VEHICLES` | `Optional[list[akkudoktoreos.devices.devices.BatteriesCommonSettings]]` | `rw` | `None` | - | +| max_electric_vehicles | `EOS_DEVICES__MAX_ELECTRIC_VEHICLES` | `Optional[int]` | `rw` | `None` | - | +| inverters | `EOS_DEVICES__INVERTERS` | `Optional[list[akkudoktoreos.devices.devices.InverterCommonSettings]]` | `rw` | `None` | - | +| max_inverters | `EOS_DEVICES__MAX_INVERTERS` | `Optional[int]` | `rw` | `None` | - | +| home_appliances | `EOS_DEVICES__HOME_APPLIANCES` | `Optional[list[akkudoktoreos.devices.devices.HomeApplianceCommonSettings]]` | `rw` | `None` | - | +| max_home_appliances | `EOS_DEVICES__MAX_HOME_APPLIANCES` | `Optional[int]` | `rw` | `None` | - | | measurement_keys | | `Optional[list[str]]` | `ro` | `N/A` | Return the measurement keys for the resource/ device stati that are measurements. | ::: @@ -337,10 +337,10 @@ Properties: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| device_id | `str` | `rw` | `` | ID of device | -| consumption_wh | `int` | `rw` | `required` | Energy consumption [Wh]. | -| duration_h | `int` | `rw` | `required` | Usage duration in hours [0 ... 24]. | -| time_windows | `Optional[akkudoktoreos.utils.datetimeutil.TimeWindowSequence]` | `rw` | `None` | Sequence of allowed time windows. Defaults to optimization general time window. | +| device_id | `str` | `rw` | `` | - | +| consumption_wh | `int` | `rw` | `required` | - | +| duration_h | `int` | `rw` | `required` | - | +| time_windows | `Optional[akkudoktoreos.utils.datetimeutil.TimeWindowSequence]` | `rw` | `None` | - | | measurement_keys | `Optional[list[str]]` | `ro` | `N/A` | Measurement keys for the home appliance stati that are measurements. | ::: @@ -367,54 +367,6 @@ Properties: } ] } - }, - { - "device_id": "ev1", - "consumption_wh": 2000, - "duration_h": 1, - "time_windows": { - "windows": [ - { - "start_time": "10:00:00.000000 Europe/Berlin", - "duration": "2 hours", - "day_of_week": null, - "date": null, - "locale": null - } - ] - } - }, - { - "device_id": "inverter1", - "consumption_wh": 2000, - "duration_h": 1, - "time_windows": { - "windows": [ - { - "start_time": "10:00:00.000000 Europe/Berlin", - "duration": "2 hours", - "day_of_week": null, - "date": null, - "locale": null - } - ] - } - }, - { - "device_id": "dishwasher", - "consumption_wh": 2000, - "duration_h": 1, - "time_windows": { - "windows": [ - { - "start_time": "10:00:00.000000 Europe/Berlin", - "duration": "2 hours", - "day_of_week": null, - "date": null, - "locale": null - } - ] - } } ] } @@ -445,57 +397,6 @@ Properties: ] }, "measurement_keys": [] - }, - { - "device_id": "ev1", - "consumption_wh": 2000, - "duration_h": 1, - "time_windows": { - "windows": [ - { - "start_time": "10:00:00.000000 Europe/Berlin", - "duration": "2 hours", - "day_of_week": null, - "date": null, - "locale": null - } - ] - }, - "measurement_keys": [] - }, - { - "device_id": "inverter1", - "consumption_wh": 2000, - "duration_h": 1, - "time_windows": { - "windows": [ - { - "start_time": "10:00:00.000000 Europe/Berlin", - "duration": "2 hours", - "day_of_week": null, - "date": null, - "locale": null - } - ] - }, - "measurement_keys": [] - }, - { - "device_id": "dishwasher", - "consumption_wh": 2000, - "duration_h": 1, - "time_windows": { - "windows": [ - { - "start_time": "10:00:00.000000 Europe/Berlin", - "duration": "2 hours", - "day_of_week": null, - "date": null, - "locale": null - } - ] - }, - "measurement_keys": [] } ] } @@ -510,9 +411,9 @@ Properties: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| device_id | `str` | `rw` | `` | ID of device | -| max_power_w | `Optional[float]` | `rw` | `None` | Maximum power [W]. | -| battery_id | `Optional[str]` | `rw` | `None` | ID of battery controlled by this inverter. | +| device_id | `str` | `rw` | `` | - | +| max_power_w | `Optional[float]` | `rw` | `None` | - | +| battery_id | `Optional[str]` | `rw` | `None` | - | | measurement_keys | `Optional[list[str]]` | `ro` | `N/A` | Measurement keys for the inverter stati that are measurements. | ::: @@ -528,21 +429,6 @@ Properties: "device_id": "battery1", "max_power_w": 10000.0, "battery_id": null - }, - { - "device_id": "ev1", - "max_power_w": 10000.0, - "battery_id": "battery1" - }, - { - "device_id": "inverter1", - "max_power_w": 10000.0, - "battery_id": "battery1" - }, - { - "device_id": "dishwasher", - "max_power_w": 10000.0, - "battery_id": "battery1" } ] } @@ -562,24 +448,6 @@ Properties: "max_power_w": 10000.0, "battery_id": null, "measurement_keys": [] - }, - { - "device_id": "ev1", - "max_power_w": 10000.0, - "battery_id": "battery1", - "measurement_keys": [] - }, - { - "device_id": "inverter1", - "max_power_w": 10000.0, - "battery_id": "battery1", - "measurement_keys": [] - }, - { - "device_id": "dishwasher", - "max_power_w": 10000.0, - "battery_id": "battery1", - "measurement_keys": [] } ] } @@ -594,16 +462,16 @@ Properties: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| device_id | `str` | `rw` | `` | ID of device | -| capacity_wh | `int` | `rw` | `8000` | Capacity [Wh]. | -| charging_efficiency | `float` | `rw` | `0.88` | Charging efficiency [0.01 ... 1.00]. | -| discharging_efficiency | `float` | `rw` | `0.88` | Discharge efficiency [0.01 ... 1.00]. | -| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh]. | -| max_charge_power_w | `Optional[float]` | `rw` | `5000` | Maximum charging power [W]. | -| min_charge_power_w | `Optional[float]` | `rw` | `50` | Minimum charging power [W]. | -| charge_rates | `Optional[numpydantic.vendor.npbase_meta_classes.NDArray]` | `rw` | `[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]` | Charge rates as factor of maximum charging power [0.00 ... 1.00]. None triggers fallback to default charge-rates. | -| min_soc_percentage | `int` | `rw` | `0` | Minimum state of charge (SOC) as percentage of capacity [%]. This is the target SoC for charging | -| max_soc_percentage | `int` | `rw` | `100` | Maximum state of charge (SOC) as percentage of capacity [%]. | +| device_id | `str` | `rw` | `` | - | +| capacity_wh | `int` | `rw` | `8000` | - | +| charging_efficiency | `float` | `rw` | `0.88` | - | +| discharging_efficiency | `float` | `rw` | `0.88` | - | +| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | - | +| max_charge_power_w | `Optional[float]` | `rw` | `5000` | - | +| min_charge_power_w | `Optional[float]` | `rw` | `50` | - | +| charge_rates | `Optional[numpydantic.vendor.npbase_meta_classes.NDArray]` | `rw` | `[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]` | - | +| min_soc_percentage | `int` | `rw` | `0` | - | +| max_soc_percentage | `int` | `rw` | `100` | - | | measurement_key_soc_factor | `str` | `ro` | `N/A` | Measurement key for the battery state of charge (SoC) as factor of total capacity [0.0 ... 1.0]. | | measurement_key_power_l1_w | `str` | `ro` | `N/A` | Measurement key for the L1 power the battery is charged or discharged with [W]. | | measurement_key_power_l2_w | `str` | `ro` | `N/A` | Measurement key for the L2 power the battery is charged or discharged with [W]. | @@ -633,42 +501,6 @@ Battery SoC, power. | "charge_rates": "[0. 0.25 0.5 0.75 1. ]", "min_soc_percentage": 10, "max_soc_percentage": 100 - }, - { - "device_id": "ev1", - "capacity_wh": 8000, - "charging_efficiency": 0.88, - "discharging_efficiency": 0.88, - "levelized_cost_of_storage_kwh": 0.12, - "max_charge_power_w": 5000.0, - "min_charge_power_w": 50.0, - "charge_rates": "[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]", - "min_soc_percentage": 10, - "max_soc_percentage": 100 - }, - { - "device_id": "inverter1", - "capacity_wh": 8000, - "charging_efficiency": 0.88, - "discharging_efficiency": 0.88, - "levelized_cost_of_storage_kwh": 0.12, - "max_charge_power_w": 5000.0, - "min_charge_power_w": 50.0, - "charge_rates": "[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]", - "min_soc_percentage": 10, - "max_soc_percentage": 100 - }, - { - "device_id": "dishwasher", - "capacity_wh": 8000, - "charging_efficiency": 0.88, - "discharging_efficiency": 0.88, - "levelized_cost_of_storage_kwh": 0.12, - "max_charge_power_w": 5000.0, - "min_charge_power_w": 50.0, - "charge_rates": "[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]", - "min_soc_percentage": 10, - "max_soc_percentage": 100 } ] } @@ -706,78 +538,6 @@ Battery SoC, power. | "battery1-power-l3-w", "battery1-power-3-phase-sym-w" ] - }, - { - "device_id": "ev1", - "capacity_wh": 8000, - "charging_efficiency": 0.88, - "discharging_efficiency": 0.88, - "levelized_cost_of_storage_kwh": 0.12, - "max_charge_power_w": 5000.0, - "min_charge_power_w": 50.0, - "charge_rates": "[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]", - "min_soc_percentage": 10, - "max_soc_percentage": 100, - "measurement_key_soc_factor": "ev1-soc-factor", - "measurement_key_power_l1_w": "ev1-power-l1-w", - "measurement_key_power_l2_w": "ev1-power-l2-w", - "measurement_key_power_l3_w": "ev1-power-l3-w", - "measurement_key_power_3_phase_sym_w": "ev1-power-3-phase-sym-w", - "measurement_keys": [ - "ev1-soc-factor", - "ev1-power-l1-w", - "ev1-power-l2-w", - "ev1-power-l3-w", - "ev1-power-3-phase-sym-w" - ] - }, - { - "device_id": "inverter1", - "capacity_wh": 8000, - "charging_efficiency": 0.88, - "discharging_efficiency": 0.88, - "levelized_cost_of_storage_kwh": 0.12, - "max_charge_power_w": 5000.0, - "min_charge_power_w": 50.0, - "charge_rates": "[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]", - "min_soc_percentage": 10, - "max_soc_percentage": 100, - "measurement_key_soc_factor": "inverter1-soc-factor", - "measurement_key_power_l1_w": "inverter1-power-l1-w", - "measurement_key_power_l2_w": "inverter1-power-l2-w", - "measurement_key_power_l3_w": "inverter1-power-l3-w", - "measurement_key_power_3_phase_sym_w": "inverter1-power-3-phase-sym-w", - "measurement_keys": [ - "inverter1-soc-factor", - "inverter1-power-l1-w", - "inverter1-power-l2-w", - "inverter1-power-l3-w", - "inverter1-power-3-phase-sym-w" - ] - }, - { - "device_id": "dishwasher", - "capacity_wh": 8000, - "charging_efficiency": 0.88, - "discharging_efficiency": 0.88, - "levelized_cost_of_storage_kwh": 0.12, - "max_charge_power_w": 5000.0, - "min_charge_power_w": 50.0, - "charge_rates": "[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]", - "min_soc_percentage": 10, - "max_soc_percentage": 100, - "measurement_key_soc_factor": "dishwasher-soc-factor", - "measurement_key_power_l1_w": "dishwasher-power-l1-w", - "measurement_key_power_l2_w": "dishwasher-power-l2-w", - "measurement_key_power_l3_w": "dishwasher-power-l3-w", - "measurement_key_power_3_phase_sym_w": "dishwasher-power-3-phase-sym-w", - "measurement_keys": [ - "dishwasher-soc-factor", - "dishwasher-power-l1-w", - "dishwasher-power-l2-w", - "dishwasher-power-l3-w", - "dishwasher-power-3-phase-sym-w" - ] } ] } @@ -792,10 +552,10 @@ Battery SoC, power. | | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| load_emr_keys | `EOS_MEASUREMENT__LOAD_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | The keys of the measurements that are energy meter readings of a load [kWh]. | -| grid_export_emr_keys | `EOS_MEASUREMENT__GRID_EXPORT_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | The keys of the measurements that are energy meter readings of energy export to grid [kWh]. | -| grid_import_emr_keys | `EOS_MEASUREMENT__GRID_IMPORT_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | The keys of the measurements that are energy meter readings of energy import from grid [kWh]. | -| pv_production_emr_keys | `EOS_MEASUREMENT__PV_PRODUCTION_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | The keys of the measurements that are PV production energy meter readings [kWh]. | +| load_emr_keys | `EOS_MEASUREMENT__LOAD_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | - | +| grid_export_emr_keys | `EOS_MEASUREMENT__GRID_EXPORT_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | - | +| grid_import_emr_keys | `EOS_MEASUREMENT__GRID_IMPORT_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | - | +| pv_production_emr_keys | `EOS_MEASUREMENT__PV_PRODUCTION_EMR_KEYS` | `Optional[list[str]]` | `rw` | `None` | - | | keys | | `list[str]` | `ro` | `N/A` | The keys of the measurements that can be stored. | ::: @@ -859,10 +619,10 @@ Battery SoC, power. | | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| horizon_hours | `EOS_OPTIMIZATION__HORIZON_HOURS` | `Optional[int]` | `rw` | `24` | The general time window within which the energy optimization goal shall be achieved [h]. Defaults to 24 hours. | -| interval | `EOS_OPTIMIZATION__INTERVAL` | `Optional[int]` | `rw` | `3600` | The optimization interval [sec]. | -| algorithm | `EOS_OPTIMIZATION__ALGORITHM` | `Optional[str]` | `rw` | `GENETIC` | The optimization algorithm. | -| genetic | `EOS_OPTIMIZATION__GENETIC` | `Optional[akkudoktoreos.optimization.optimization.GeneticCommonSettings]` | `rw` | `None` | Genetic optimization algorithm configuration. | +| horizon_hours | `EOS_OPTIMIZATION__HORIZON_HOURS` | `Optional[int]` | `rw` | `24` | - | +| interval | `EOS_OPTIMIZATION__INTERVAL` | `Optional[int]` | `rw` | `3600` | - | +| algorithm | `EOS_OPTIMIZATION__ALGORITHM` | `Optional[str]` | `rw` | `GENETIC` | - | +| genetic | `EOS_OPTIMIZATION__GENETIC` | `Optional[akkudoktoreos.optimization.optimization.GeneticCommonSettings]` | `rw` | `None` | - | ::: ### Example Input/Output @@ -895,10 +655,10 @@ Battery SoC, power. | | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| individuals | `Optional[int]` | `rw` | `300` | Number of individuals (solutions) to generate for the (initial) generation [>= 10]. Defaults to 300. | -| generations | `Optional[int]` | `rw` | `400` | Number of generations to evaluate the optimal solution [>= 10]. Defaults to 400. | -| seed | `Optional[int]` | `rw` | `None` | Fixed seed for genetic algorithm. Defaults to 'None' which means random seed. | -| penalties | `Optional[dict[str, Union[float, int, str]]]` | `rw` | `None` | A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value. | +| individuals | `Optional[int]` | `rw` | `300` | - | +| generations | `Optional[int]` | `rw` | `400` | - | +| seed | `Optional[int]` | `rw` | `None` | - | +| penalties | `Optional[dict[str, Union[float, int, str]]]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -942,8 +702,8 @@ Validators: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| hours | `EOS_PREDICTION__HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the future for predictions | -| historic_hours | `EOS_PREDICTION__HISTORIC_HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the past for historical predictions data | +| hours | `EOS_PREDICTION__HOURS` | `Optional[int]` | `rw` | `48` | - | +| historic_hours | `EOS_PREDICTION__HISTORIC_HOURS` | `Optional[int]` | `rw` | `48` | - | ::: ### Example Input/Output @@ -967,10 +727,10 @@ Validators: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| provider | `EOS_ELECPRICE__PROVIDER` | `Optional[str]` | `rw` | `None` | Electricity price provider id of provider to be used. | -| charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges [€/kWh]. Will be added to variable market price. | -| vat_rate | `EOS_ELECPRICE__VAT_RATE` | `Optional[float]` | `rw` | `1.19` | VAT rate factor applied to electricity price when charges are used. | -| provider_settings | `EOS_ELECPRICE__PROVIDER_SETTINGS` | `ElecPriceCommonProviderSettings` | `rw` | `required` | Provider settings | +| provider | `EOS_ELECPRICE__PROVIDER` | `Optional[str]` | `rw` | `None` | - | +| charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | - | +| vat_rate | `EOS_ELECPRICE__VAT_RATE` | `Optional[float]` | `rw` | `1.19` | - | +| provider_settings | `EOS_ELECPRICE__PROVIDER_SETTINGS` | `ElecPriceCommonProviderSettings` | `rw` | `required` | - | ::: ### Example Input/Output @@ -998,8 +758,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import elecprice data from. | -| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of electricity price forecast value lists. | +| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | - | +| import_json | `Optional[str]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1027,7 +787,7 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| ElecPriceImport | `Optional[akkudoktoreos.prediction.elecpriceimport.ElecPriceImportCommonSettings]` | `rw` | `None` | ElecPriceImport settings | +| ElecPriceImport | `Optional[akkudoktoreos.prediction.elecpriceimport.ElecPriceImportCommonSettings]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1052,8 +812,8 @@ Validators: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| provider | `EOS_FEEDINTARIFF__PROVIDER` | `Optional[str]` | `rw` | `None` | Feed in tariff provider id of provider to be used. | -| provider_settings | `EOS_FEEDINTARIFF__PROVIDER_SETTINGS` | `FeedInTariffCommonProviderSettings` | `rw` | `required` | Provider settings | +| provider | `EOS_FEEDINTARIFF__PROVIDER` | `Optional[str]` | `rw` | `None` | - | +| provider_settings | `EOS_FEEDINTARIFF__PROVIDER_SETTINGS` | `FeedInTariffCommonProviderSettings` | `rw` | `required` | - | ::: ### Example Input/Output @@ -1080,8 +840,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import feed in tariff data from. | -| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of feed in tariff forecast value lists. | +| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | - | +| import_json | `Optional[str]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1109,7 +869,7 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| feed_in_tariff_kwh | `Optional[float]` | `rw` | `None` | Electricity price feed in tariff [€/kWH]. | +| feed_in_tariff_kwh | `Optional[float]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1136,8 +896,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| FeedInTariffFixed | `Optional[akkudoktoreos.prediction.feedintarifffixed.FeedInTariffFixedCommonSettings]` | `rw` | `None` | FeedInTariffFixed settings | -| FeedInTariffImport | `Optional[akkudoktoreos.prediction.feedintariffimport.FeedInTariffImportCommonSettings]` | `rw` | `None` | FeedInTariffImport settings | +| FeedInTariffFixed | `Optional[akkudoktoreos.prediction.feedintarifffixed.FeedInTariffFixedCommonSettings]` | `rw` | `None` | - | +| FeedInTariffImport | `Optional[akkudoktoreos.prediction.feedintariffimport.FeedInTariffImportCommonSettings]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1163,8 +923,8 @@ Validators: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| provider | `EOS_LOAD__PROVIDER` | `Optional[str]` | `rw` | `None` | Load provider id of provider to be used. | -| provider_settings | `EOS_LOAD__PROVIDER_SETTINGS` | `LoadCommonProviderSettings` | `rw` | `required` | Provider settings | +| provider | `EOS_LOAD__PROVIDER` | `Optional[str]` | `rw` | `None` | - | +| provider_settings | `EOS_LOAD__PROVIDER_SETTINGS` | `LoadCommonProviderSettings` | `rw` | `required` | - | ::: ### Example Input/Output @@ -1192,8 +952,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import load data from. | -| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of load forecast value lists. | +| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | - | +| import_json | `Optional[str]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1221,8 +981,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| load_vrm_token | `str` | `rw` | `your-token` | Token for Connecting VRM API | -| load_vrm_idsite | `int` | `rw` | `12345` | VRM-Installation-ID | +| load_vrm_token | `str` | `rw` | `your-token` | - | +| load_vrm_idsite | `int` | `rw` | `12345` | - | ::: #### Example Input/Output @@ -1250,7 +1010,7 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| loadakkudoktor_year_energy_kwh | `Optional[float]` | `rw` | `None` | Yearly energy consumption (kWh). | +| loadakkudoktor_year_energy_kwh | `Optional[float]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1277,9 +1037,9 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| LoadAkkudoktor | `Optional[akkudoktoreos.prediction.loadakkudoktor.LoadAkkudoktorCommonSettings]` | `rw` | `None` | LoadAkkudoktor settings | -| LoadVrm | `Optional[akkudoktoreos.prediction.loadvrm.LoadVrmCommonSettings]` | `rw` | `None` | LoadVrm settings | -| LoadImport | `Optional[akkudoktoreos.prediction.loadimport.LoadImportCommonSettings]` | `rw` | `None` | LoadImport settings | +| LoadAkkudoktor | `Optional[akkudoktoreos.prediction.loadakkudoktor.LoadAkkudoktorCommonSettings]` | `rw` | `None` | - | +| LoadVrm | `Optional[akkudoktoreos.prediction.loadvrm.LoadVrmCommonSettings]` | `rw` | `None` | - | +| LoadImport | `Optional[akkudoktoreos.prediction.loadimport.LoadImportCommonSettings]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1306,10 +1066,10 @@ Validators: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| provider | `EOS_PVFORECAST__PROVIDER` | `Optional[str]` | `rw` | `None` | PVForecast provider id of provider to be used. | -| provider_settings | `EOS_PVFORECAST__PROVIDER_SETTINGS` | `PVForecastCommonProviderSettings` | `rw` | `required` | Provider settings | -| planes | `EOS_PVFORECAST__PLANES` | `Optional[list[akkudoktoreos.prediction.pvforecast.PVForecastPlaneSetting]]` | `rw` | `None` | Plane configuration. | -| max_planes | `EOS_PVFORECAST__MAX_PLANES` | `Optional[int]` | `rw` | `0` | Maximum number of planes that can be set | +| provider | `EOS_PVFORECAST__PROVIDER` | `Optional[str]` | `rw` | `None` | - | +| provider_settings | `EOS_PVFORECAST__PROVIDER_SETTINGS` | `PVForecastCommonProviderSettings` | `rw` | `required` | - | +| planes | `EOS_PVFORECAST__PLANES` | `Optional[list[akkudoktoreos.prediction.pvforecast.PVForecastPlaneSetting]]` | `rw` | `None` | - | +| max_planes | `EOS_PVFORECAST__MAX_PLANES` | `Optional[int]` | `rw` | `0` | - | | planes_peakpower | | `List[float]` | `ro` | `N/A` | Compute a list of the peak power per active planes. | | planes_azimuth | | `List[float]` | `ro` | `N/A` | Compute a list of the azimuths per active planes. | | planes_tilt | | `List[float]` | `ro` | `N/A` | Compute a list of the tilts per active planes. | @@ -1479,22 +1239,22 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| surface_tilt | `Optional[float]` | `rw` | `30.0` | Tilt angle from horizontal plane. Ignored for two-axis tracking. | -| surface_azimuth | `Optional[float]` | `rw` | `180.0` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). | -| userhorizon | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. | -| peakpower | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. | -| pvtechchoice | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. | -| mountingplace | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. | -| loss | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent | -| trackingtype | `Optional[int]` | `rw` | `None` | Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south. | -| optimal_surface_tilt | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. | -| optimalangles | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. | -| albedo | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. | -| module_model | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. | -| inverter_model | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. | -| inverter_paco | `Optional[int]` | `rw` | `None` | AC power rating of the inverter [W]. | -| modules_per_string | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. | -| strings_per_inverter | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. | +| surface_tilt | `Optional[float]` | `rw` | `30.0` | - | +| surface_azimuth | `Optional[float]` | `rw` | `180.0` | - | +| userhorizon | `Optional[List[float]]` | `rw` | `None` | - | +| peakpower | `Optional[float]` | `rw` | `None` | - | +| pvtechchoice | `Optional[str]` | `rw` | `crystSi` | - | +| mountingplace | `Optional[str]` | `rw` | `free` | - | +| loss | `Optional[float]` | `rw` | `14.0` | - | +| trackingtype | `Optional[int]` | `rw` | `None` | - | +| optimal_surface_tilt | `Optional[bool]` | `rw` | `False` | - | +| optimalangles | `Optional[bool]` | `rw` | `False` | - | +| albedo | `Optional[float]` | `rw` | `None` | - | +| module_model | `Optional[str]` | `rw` | `None` | - | +| inverter_model | `Optional[str]` | `rw` | `None` | - | +| inverter_paco | `Optional[int]` | `rw` | `None` | - | +| modules_per_string | `Optional[int]` | `rw` | `None` | - | +| strings_per_inverter | `Optional[int]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1526,28 +1286,6 @@ Validators: "inverter_paco": 6000, "modules_per_string": 20, "strings_per_inverter": 2 - }, - { - "surface_tilt": 20.0, - "surface_azimuth": 90.0, - "userhorizon": [ - 5.0, - 15.0, - 25.0 - ], - "peakpower": 3.5, - "pvtechchoice": "crystSi", - "mountingplace": "free", - "loss": 14.0, - "trackingtype": 1, - "optimal_surface_tilt": false, - "optimalangles": false, - "albedo": null, - "module_model": null, - "inverter_model": null, - "inverter_paco": 4000, - "modules_per_string": 20, - "strings_per_inverter": 2 } ] } @@ -1562,8 +1300,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| pvforecast_vrm_token | `str` | `rw` | `your-token` | Token for Connecting VRM API | -| pvforecast_vrm_idsite | `int` | `rw` | `12345` | VRM-Installation-ID | +| pvforecast_vrm_token | `str` | `rw` | `your-token` | - | +| pvforecast_vrm_idsite | `int` | `rw` | `12345` | - | ::: #### Example Input/Output @@ -1591,8 +1329,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import PV forecast data from. | -| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of PV forecast value lists. | +| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | - | +| import_json | `Optional[str]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1620,8 +1358,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| PVForecastImport | `Optional[akkudoktoreos.prediction.pvforecastimport.PVForecastImportCommonSettings]` | `rw` | `None` | PVForecastImport settings | -| PVForecastVrm | `Optional[akkudoktoreos.prediction.pvforecastvrm.PVForecastVrmCommonSettings]` | `rw` | `None` | PVForecastVrm settings | +| PVForecastImport | `Optional[akkudoktoreos.prediction.pvforecastimport.PVForecastImportCommonSettings]` | `rw` | `None` | - | +| PVForecastVrm | `Optional[akkudoktoreos.prediction.pvforecastvrm.PVForecastVrmCommonSettings]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1647,8 +1385,8 @@ Validators: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| provider | `EOS_WEATHER__PROVIDER` | `Optional[str]` | `rw` | `None` | Weather provider id of provider to be used. | -| provider_settings | `EOS_WEATHER__PROVIDER_SETTINGS` | `WeatherCommonProviderSettings` | `rw` | `required` | Provider settings | +| provider | `EOS_WEATHER__PROVIDER` | `Optional[str]` | `rw` | `None` | - | +| provider_settings | `EOS_WEATHER__PROVIDER_SETTINGS` | `WeatherCommonProviderSettings` | `rw` | `required` | - | ::: ### Example Input/Output @@ -1674,8 +1412,8 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import weather data from. | -| import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of weather forecast value lists. | +| import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | - | +| import_json | `Optional[str]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1703,7 +1441,7 @@ Validators: | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| WeatherImport | `Optional[akkudoktoreos.prediction.weatherimport.WeatherImportCommonSettings]` | `rw` | `None` | WeatherImport settings | +| WeatherImport | `Optional[akkudoktoreos.prediction.weatherimport.WeatherImportCommonSettings]` | `rw` | `None` | - | ::: #### Example Input/Output @@ -1728,12 +1466,12 @@ Validators: | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| host | `EOS_SERVER__HOST` | `Optional[str]` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. | -| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. | -| verbose | `EOS_SERVER__VERBOSE` | `Optional[bool]` | `rw` | `False` | Enable debug output | -| startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. Defaults to True. | -| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[str]` | `rw` | `None` | EOSdash server IP address. Defaults to EOS server IP address. | -| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `None` | EOSdash server IP port number. Defaults to EOS server IP port number + 1. | +| host | `EOS_SERVER__HOST` | `Optional[str]` | `rw` | `127.0.0.1` | - | +| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | - | +| verbose | `EOS_SERVER__VERBOSE` | `Optional[bool]` | `rw` | `False` | - | +| startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | - | +| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[str]` | `rw` | `None` | - | +| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `None` | - | ::: ### Example Input/Output diff --git a/scripts/generate_config_md.py b/scripts/generate_config_md.py index 12ba815..20610ae 100755 --- a/scripts/generate_config_md.py +++ b/scripts/generate_config_md.py @@ -53,17 +53,49 @@ def resolve_nested_types(field_type: Any, parent_types: list[str]) -> list[tuple def get_example_or_default(field_name: str, field_info: FieldInfo, example_ix: int) -> Any: - """Generate a default value for a field, considering constraints.""" - if field_info.examples is not None: - try: - return field_info.examples[example_ix] - except IndexError: - return field_info.examples[-1] + """Generate a default value for a field, considering constraints. - if field_info.default is not None: + Priority: + 1. field_info.examples + 2. field_info.example + 3. json_schema_extra['examples'] + 4. json_schema_extra['example'] + 5. field_info.default + """ + # 1. Old-style examples attribute + examples = getattr(field_info, "examples", None) + if examples is not None: + try: + return examples[example_ix] + except IndexError: + return examples[-1] + + # 2. Old-style single example + example = getattr(field_info, "example", None) + if example is not None: + return example + + # 3. Look into json_schema_extra (new style) + extra = getattr(field_info, "json_schema_extra", {}) or {} + + examples = extra.get("examples") + if examples is not None: + try: + return examples[example_ix] + except IndexError: + return examples[-1] + + example = extra.get("example") + if example is not None: + return example + + # 5. Default + if getattr(field_info, "default", None) not in (None, ...): return field_info.default - raise NotImplementedError(f"No default or example provided '{field_name}': {field_info}") + raise NotImplementedError( + f"No default or example provided for field '{field_name}': {field_info}" + ) def get_model_structure_from_examples( diff --git a/src/akkudoktoreos/config/config.py b/src/akkudoktoreos/config/config.py index 32d754f..7183907 100644 --- a/src/akkudoktoreos/config/config.py +++ b/src/akkudoktoreos/config/config.py @@ -85,28 +85,38 @@ class GeneralSettings(SettingsBaseModel): _config_file_path: ClassVar[Optional[Path]] = None version: str = Field( - default=__version__, description="Configuration file version. Used to check compatibility." + default=__version__, + json_schema_extra={ + "description": "Configuration file version. Used to check compatibility." + }, ) data_folder_path: Optional[Path] = Field( - default=None, description="Path to EOS data directory.", examples=[None, "/home/eos/data"] + default=None, + json_schema_extra={ + "description": "Path to EOS data directory.", + "examples": [None, "/home/eos/data"], + }, ) data_output_subpath: Optional[Path] = Field( - default="output", description="Sub-path for the EOS output data directory." + default="output", + json_schema_extra={"description": "Sub-path for the EOS output data directory."}, ) latitude: Optional[float] = Field( default=52.52, ge=-90.0, le=90.0, - description="Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)", + json_schema_extra={ + "description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)" + }, ) longitude: Optional[float] = Field( default=13.405, ge=-180.0, le=180.0, - description="Longitude in decimal degrees, within -180 to 180 (°)", + json_schema_extra={"description": "Longitude in decimal degrees, within -180 to 180 (°)"}, ) # Computed fields @@ -158,64 +168,49 @@ class SettingsEOS(pydantic_settings.BaseSettings, PydanticModelNestedValueMixin) """ general: Optional[GeneralSettings] = Field( - default=None, - description="General Settings", + default=None, json_schema_extra={"description": "General Settings"} ) cache: Optional[CacheCommonSettings] = Field( - default=None, - description="Cache Settings", + default=None, json_schema_extra={"description": "Cache Settings"} ) ems: Optional[EnergyManagementCommonSettings] = Field( - default=None, - description="Energy Management Settings", + default=None, json_schema_extra={"description": "Energy Management Settings"} ) logging: Optional[LoggingCommonSettings] = Field( - default=None, - description="Logging Settings", + default=None, json_schema_extra={"description": "Logging Settings"} ) devices: Optional[DevicesCommonSettings] = Field( - default=None, - description="Devices Settings", + default=None, json_schema_extra={"description": "Devices Settings"} ) measurement: Optional[MeasurementCommonSettings] = Field( - default=None, - description="Measurement Settings", + default=None, json_schema_extra={"description": "Measurement Settings"} ) optimization: Optional[OptimizationCommonSettings] = Field( - default=None, - description="Optimization Settings", + default=None, json_schema_extra={"description": "Optimization Settings"} ) prediction: Optional[PredictionCommonSettings] = Field( - default=None, - description="Prediction Settings", + default=None, json_schema_extra={"description": "Prediction Settings"} ) elecprice: Optional[ElecPriceCommonSettings] = Field( - default=None, - description="Electricity Price Settings", + default=None, json_schema_extra={"description": "Electricity Price Settings"} ) feedintariff: Optional[FeedInTariffCommonSettings] = Field( - default=None, - description="Feed In Tariff Settings", + default=None, json_schema_extra={"description": "Feed In Tariff Settings"} ) load: Optional[LoadCommonSettings] = Field( - default=None, - description="Load Settings", + default=None, json_schema_extra={"description": "Load Settings"} ) pvforecast: Optional[PVForecastCommonSettings] = Field( - default=None, - description="PV Forecast Settings", + default=None, json_schema_extra={"description": "PV Forecast Settings"} ) weather: Optional[WeatherCommonSettings] = Field( - default=None, - description="Weather Settings", + default=None, json_schema_extra={"description": "Weather Settings"} ) server: Optional[ServerCommonSettings] = Field( - default=None, - description="Server Settings", + default=None, json_schema_extra={"description": "Server Settings"} ) utils: Optional[UtilsCommonSettings] = Field( - default=None, - description="Utilities Settings", + default=None, json_schema_extra={"description": "Utilities Settings"} ) model_config = pydantic_settings.SettingsConfigDict( diff --git a/src/akkudoktoreos/core/cache.py b/src/akkudoktoreos/core/cache.py index 8b72a09..aa392fc 100644 --- a/src/akkudoktoreos/core/cache.py +++ b/src/akkudoktoreos/core/cache.py @@ -251,10 +251,14 @@ RetType = TypeVar("RetType") class CacheFileRecord(PydanticBaseModel): - cache_file: Any = Field(..., description="File descriptor of the cache file.") - until_datetime: DateTime = Field(..., description="Datetime until the cache file is valid.") + cache_file: Any = Field( + ..., json_schema_extra={"description": "File descriptor of the cache file."} + ) + until_datetime: DateTime = Field( + ..., json_schema_extra={"description": "Datetime until the cache file is valid."} + ) ttl_duration: Optional[Duration] = Field( - default=None, description="Duration the cache file is valid." + default=None, json_schema_extra={"description": "Duration the cache file is valid."} ) diff --git a/src/akkudoktoreos/core/cachesettings.py b/src/akkudoktoreos/core/cachesettings.py index faa6153..e3581eb 100644 --- a/src/akkudoktoreos/core/cachesettings.py +++ b/src/akkudoktoreos/core/cachesettings.py @@ -15,11 +15,13 @@ class CacheCommonSettings(SettingsBaseModel): """Cache Configuration.""" subpath: Optional[Path] = Field( - default="cache", description="Sub-path for the EOS cache data directory." + default="cache", + json_schema_extra={"description": "Sub-path for the EOS cache data directory."}, ) cleanup_interval: float = Field( - default=5 * 60, description="Intervall in seconds for EOS file cache cleanup." + default=5 * 60, + json_schema_extra={"description": "Intervall in seconds for EOS file cache cleanup."}, ) # Do not make this a pydantic computed field. The pydantic model must be fully initialized diff --git a/src/akkudoktoreos/core/dataabc.py b/src/akkudoktoreos/core/dataabc.py index 31968d9..8f65c51 100644 --- a/src/akkudoktoreos/core/dataabc.py +++ b/src/akkudoktoreos/core/dataabc.py @@ -84,12 +84,16 @@ class DataRecord(DataBase, MutableMapping): - Supports non-standard data types like `datetime`. """ - date_time: Optional[DateTime] = Field(default=None, description="DateTime") + date_time: Optional[DateTime] = Field( + default=None, json_schema_extra={"description": "DateTime"} + ) configured_data: dict[str, Any] = Field( default_factory=dict, - description="Configured field like data", - examples=[{"load0_mr": 40421}], + json_schema_extra={ + "description": "Configured field like data", + "examples": [{"load0_mr": 40421}], + }, ) # Pydantic v2 model configuration @@ -368,10 +372,11 @@ class DataRecord(DataBase, MutableMapping): return None # Get all descriptions from the fields - descriptions = { - field_name: field_info.description - for field_name, field_info in cls.model_fields.items() - } + descriptions: dict[str, str] = {} + for field_name in cls.model_fields.keys(): + desc = cls.field_description(field_name) + if desc: + descriptions[field_name] = desc # Use difflib to get close matches matches = difflib.get_close_matches( @@ -429,8 +434,7 @@ class DataSequence(DataBase, MutableSequence): Usage: # Example of creating, adding, and using DataSequence class DerivedSequence(DataSquence): - records: List[DerivedDataRecord] = Field(default_factory=list, - description="List of data records") + records: List[DerivedDataRecord] = Field(default_factory=list, json_schema_extra={ "description": "List of data records" }) seq = DerivedSequence() seq.insert(DerivedDataRecord(date_time=datetime.now(), temperature=72)) @@ -445,7 +449,9 @@ class DataSequence(DataBase, MutableSequence): """ # To be overloaded by derived classes. - records: List[DataRecord] = Field(default_factory=list, description="List of data records") + records: List[DataRecord] = Field( + default_factory=list, json_schema_extra={"description": "List of data records"} + ) # Derived fields (computed) @computed_field # type: ignore[prop-decorator] @@ -1313,7 +1319,7 @@ class DataProvider(SingletonMixin, DataSequence): """ update_datetime: Optional[AwareDatetime] = Field( - None, description="Latest update datetime for generic data" + None, json_schema_extra={"description": "Latest update datetime for generic data"} ) @abstractmethod @@ -1780,7 +1786,7 @@ class DataContainer(SingletonMixin, DataBase, MutableMapping): # To be overloaded by derived classes. providers: List[DataProvider] = Field( - default_factory=list, description="List of data providers" + default_factory=list, json_schema_extra={"description": "List of data providers"} ) @field_validator("providers", mode="after") diff --git a/src/akkudoktoreos/core/emplan.py b/src/akkudoktoreos/core/emplan.py index 49772ce..2704d2d 100644 --- a/src/akkudoktoreos/core/emplan.py +++ b/src/akkudoktoreos/core/emplan.py @@ -330,10 +330,13 @@ class PowerValue(PydanticBaseModel): """ commodity_quantity: CommodityQuantity = Field( - ..., description="The power quantity the value refers to." + ..., json_schema_extra={"description": "The power quantity the value refers to."} ) value: float = Field( - ..., description="Power value expressed in the unit associated with the CommodityQuantity." + ..., + json_schema_extra={ + "description": "Power value expressed in the unit associated with the CommodityQuantity." + }, ) @@ -347,31 +350,45 @@ class PowerForecastValue(PydanticBaseModel): value_upper_limit: Optional[float] = Field( None, - description="The upper boundary of the range with 100% certainty the power value is in it.", + json_schema_extra={ + "description": "The upper boundary of the range with 100% certainty the power value is in it." + }, ) value_upper_95PPR: Optional[float] = Field( None, - description="The upper boundary of the range with 95% certainty the power value is in it.", + json_schema_extra={ + "description": "The upper boundary of the range with 95% certainty the power value is in it." + }, ) value_upper_68PPR: Optional[float] = Field( None, - description="The upper boundary of the range with 68% certainty the power value is in it.", + json_schema_extra={ + "description": "The upper boundary of the range with 68% certainty the power value is in it." + }, + ) + value_expected: float = Field( + ..., json_schema_extra={"description": "The expected power value."} ) - value_expected: float = Field(..., description="The expected power value.") value_lower_68PPR: Optional[float] = Field( None, - description="The lower boundary of the range with 68% certainty the power value is in it.", + json_schema_extra={ + "description": "The lower boundary of the range with 68% certainty the power value is in it." + }, ) value_lower_95PPR: Optional[float] = Field( None, - description="The lower boundary of the range with 95% certainty the power value is in it.", + json_schema_extra={ + "description": "The lower boundary of the range with 95% certainty the power value is in it." + }, ) value_lower_limit: Optional[float] = Field( None, - description="The lower boundary of the range with 100% certainty the power value is in it.", + json_schema_extra={ + "description": "The lower boundary of the range with 100% certainty the power value is in it." + }, ) commodity_quantity: CommodityQuantity = Field( - ..., description="The power quantity the value refers to." + ..., json_schema_extra={"description": "The power quantity the value refers to."} ) @@ -384,11 +401,13 @@ class PowerRange(PydanticBaseModel): """ start_of_range: float = Field( - ..., description="Power value that defines the start of the range." + ..., json_schema_extra={"description": "Power value that defines the start of the range."} + ) + end_of_range: float = Field( + ..., json_schema_extra={"description": "Power value that defines the end of the range."} ) - end_of_range: float = Field(..., description="Power value that defines the end of the range.") commodity_quantity: CommodityQuantity = Field( - ..., description="The power quantity the values refer to." + ..., json_schema_extra={"description": "The power quantity the values refer to."} ) @@ -400,8 +419,12 @@ class NumberRange(PydanticBaseModel): Used for representing ranges of prices, percentages, or other numeric values. """ - start_of_range: float = Field(..., description="Number that defines the start of the range.") - end_of_range: float = Field(..., description="Number that defines the end of the range.") + start_of_range: float = Field( + ..., json_schema_extra={"description": "Number that defines the start of the range."} + ) + end_of_range: float = Field( + ..., json_schema_extra={"description": "Number that defines the end of the range."} + ) class PowerMeasurement(PydanticBaseModel): @@ -415,11 +438,13 @@ class PowerMeasurement(PydanticBaseModel): type: Literal["PowerMeasurement"] = Field(default="PowerMeasurement") measurement_timestamp: DateTime = Field( - ..., description="Timestamp when PowerValues were measured." + ..., json_schema_extra={"description": "Timestamp when PowerValues were measured."} ) values: list[PowerValue] = Field( ..., - description="Array of measured PowerValues. Shall contain at least one item and at most one item per 'commodity_quantity' (defined inside the PowerValue).", + json_schema_extra={ + "description": "Array of measured PowerValues. Shall contain at least one item and at most one item per 'commodity_quantity' (defined inside the PowerValue)." + }, ) @@ -438,15 +463,19 @@ class EnergyMeasurement(PydanticBaseModel): type: Literal["EnergyMeasurement"] = Field(default="EnergyMeasurement") measurement_timestamp: DateTime = Field( - ..., description="Timestamp when energy values were measured." + ..., json_schema_extra={"description": "Timestamp when energy values were measured."} ) last_reset: Optional[DateTime] = Field( default=None, - description="Timestamp when the energy meter's cumulative counter was last reset.", + json_schema_extra={ + "description": "Timestamp when the energy meter's cumulative counter was last reset." + }, ) values: list[PowerValue] = Field( ..., - description="Array of measured energy values. Shall contain at least one item and at most one item per 'commodity_quantity' (defined inside the PowerValue).", + json_schema_extra={ + "description": "Array of measured energy values. Shall contain at least one item and at most one item per 'commodity_quantity' (defined inside the PowerValue)." + }, ) @@ -458,8 +487,12 @@ class Role(PydanticBaseModel): an entity interacts with the energy system for that commodity. """ - role: RoleType = Field(..., description="Role type for the given commodity.") - commodity: Commodity = Field(..., description="Commodity the role refers to.") + role: RoleType = Field( + ..., json_schema_extra={"description": "Role type for the given commodity."} + ) + commodity: Commodity = Field( + ..., json_schema_extra={"description": "Commodity the role refers to."} + ) class ReceptionStatus(PydanticBaseModel): @@ -471,14 +504,19 @@ class ReceptionStatus(PydanticBaseModel): """ status: ReceptionStatusValues = Field( - ..., description="Enumeration of status values indicating reception outcome." + ..., + json_schema_extra={ + "description": "Enumeration of status values indicating reception outcome." + }, ) diagnostic_label: Optional[str] = Field( None, - description=( - "Optional diagnostic label providing additional information for debugging. " - "Not intended for Human-Machine Interface (HMI) use." - ), + json_schema_extra={ + "description": ( + "Optional diagnostic label providing additional information for debugging. " + "Not intended for Human-Machine Interface (HMI) use." + ) + }, ) @@ -493,57 +531,73 @@ class Transition(PydanticBaseModel): id: ID = Field( ..., - description=( - "ID of the Transition. Shall be unique in the scope of the OMBC.SystemDescription, " - "FRBC.ActuatorDescription, or DDBC.ActuatorDescription in which it is used." - ), + json_schema_extra={ + "description": ( + "ID of the Transition. Shall be unique in the scope of the OMBC.SystemDescription, " + "FRBC.ActuatorDescription, or DDBC.ActuatorDescription in which it is used." + ) + }, ) from_: ID = Field( ..., alias="from", - description=( - "ID of the OperationMode that should be switched from. " - "Exact type depends on the ControlType." - ), + json_schema_extra={ + "description": ( + "ID of the OperationMode that should be switched from. " + "Exact type depends on the ControlType." + ) + }, ) to: ID = Field( ..., - description=( - "ID of the OperationMode that will be switched to. " - "Exact type depends on the ControlType." - ), + json_schema_extra={ + "description": ( + "ID of the OperationMode that will be switched to. " + "Exact type depends on the ControlType." + ) + }, ) start_timers: list[ID] = Field( ..., - description=( - "List of IDs of Timers that will be (re)started when this Transition is initiated." - ), + json_schema_extra={ + "description": ( + "List of IDs of Timers that will be (re)started when this Transition is initiated." + ) + }, ) blocking_timers: list[ID] = Field( ..., - description=( - "List of IDs of Timers that block this Transition from initiating " - "while at least one of them is not yet finished." - ), + json_schema_extra={ + "description": ( + "List of IDs of Timers that block this Transition from initiating " + "while at least one of them is not yet finished." + ) + }, ) transition_costs: Optional[float] = Field( None, - description=( - "Absolute costs for going through this Transition, in the currency defined in ResourceManagerDetails." - ), + json_schema_extra={ + "description": ( + "Absolute costs for going through this Transition, in the currency defined in ResourceManagerDetails." + ) + }, ) transition_duration: Optional[Duration] = Field( None, - description=( - "Time between initiation of this Transition and when the device behaves according to the target Operation Mode. " - "Assumed negligible if not provided." - ), + json_schema_extra={ + "description": ( + "Time between initiation of this Transition and when the device behaves according to the target Operation Mode. " + "Assumed negligible if not provided." + ) + }, ) abnormal_condition_only: bool = Field( ..., - description=( - "Indicates whether this Transition may only be used during an abnormal condition." - ), + json_schema_extra={ + "description": ( + "Indicates whether this Transition may only be used during an abnormal condition." + ) + }, ) model_config = { @@ -563,29 +617,38 @@ class Timer(PydanticBaseModel): id: ID = Field( ..., - description=( - "ID of the Timer. Shall be unique in the scope of the OMBC.SystemDescription, " - "FRBC.ActuatorDescription, or DDBC.ActuatorDescription in which it is used." - ), + json_schema_extra={ + "description": ( + "ID of the Timer. Shall be unique in the scope of the OMBC.SystemDescription, " + "FRBC.ActuatorDescription, or DDBC.ActuatorDescription in which it is used." + ) + }, ) diagnostic_label: Optional[str] = Field( None, - description=( - "Human readable name/description of the Timer. " - "This element is only intended for diagnostic purposes and not for HMI applications." - ), + json_schema_extra={ + "description": ( + "Human readable name/description of the Timer. " + "This element is only intended for diagnostic purposes and not for HMI applications." + ) + }, ) duration: Duration = Field( - ..., description=("The time it takes for the Timer to finish after it has been started.") + ..., + json_schema_extra={ + "description": ("The time it takes for the Timer to finish after it has been started.") + }, ) finished_at: DateTime = Field( ..., - description=( - "Timestamp indicating when the Timer will be finished. " - "If in the future, the timer is not yet finished. " - "If in the past, the timer is finished. " - "If the timer was never started, this can be an arbitrary timestamp in the past." - ), + json_schema_extra={ + "description": ( + "Timestamp indicating when the Timer will be finished. " + "If in the future, the timer is not yet finished. " + "If in the past, the timer is finished. " + "If the timer was never started, this can be an arbitrary timestamp in the past." + ) + }, ) @@ -597,9 +660,15 @@ class InstructionStatusUpdate(PydanticBaseModel): execution and provides feedback about the system's response to control commands. """ - instruction_id: ID = Field(..., description=("ID of this instruction, as provided by the CEM.")) - status_type: InstructionStatus = Field(..., description=("Present status of this instruction.")) - timestamp: DateTime = Field(..., description=("Timestamp when the status_type last changed.")) + instruction_id: ID = Field( + ..., json_schema_extra={"description": ("ID of this instruction, as provided by the CEM.")} + ) + status_type: InstructionStatus = Field( + ..., json_schema_extra={"description": ("Present status of this instruction.")} + ) + timestamp: DateTime = Field( + ..., json_schema_extra={"description": ("Timestamp when the status_type last changed.")} + ) # ResourceManager @@ -616,36 +685,60 @@ class ResourceManagerDetails(PydanticBaseModel): resource_id: ID = Field( ..., - description="Identifier of the ResourceManager. Shall be unique within the scope of the CEM.", + json_schema_extra={ + "description": "Identifier of the ResourceManager. Shall be unique within the scope of the CEM." + }, + ) + name: Optional[str] = Field( + None, json_schema_extra={"description": "Human readable name given by user."} ) - name: Optional[str] = Field(None, description="Human readable name given by user.") roles: list[Role] = Field( - ..., description="Each ResourceManager provides one or more energy Roles." + ..., + json_schema_extra={ + "description": "Each ResourceManager provides one or more energy Roles." + }, + ) + manufacturer: Optional[str] = Field( + None, json_schema_extra={"description": "Name of Manufacturer."} + ) + model: Optional[str] = Field( + None, json_schema_extra={"description": "Name of the model of the device."} + ) + serial_number: Optional[str] = Field( + None, json_schema_extra={"description": "Serial number of the device."} ) - manufacturer: Optional[str] = Field(None, description="Name of Manufacturer.") - model: Optional[str] = Field(None, description="Name of the model of the device.") - serial_number: Optional[str] = Field(None, description="Serial number of the device.") firmware_version: Optional[str] = Field( - None, description="Version identifier of the firmware used in the device." + None, + json_schema_extra={"description": "Version identifier of the firmware used in the device."}, ) instruction_processing_delay: Duration = Field( ..., - description="The average time the system and device needs to process and execute an instruction.", + json_schema_extra={ + "description": "The average time the system and device needs to process and execute an instruction." + }, ) available_control_types: list[ControlType] = Field( - ..., description="The control types supported by this ResourceManager." + ..., + json_schema_extra={"description": "The control types supported by this ResourceManager."}, ) currency: Optional[Currency] = Field( None, - description="Currency to be used for all information regarding costs. " - "Mandatory if cost information is published.", + json_schema_extra={ + "description": "Currency to be used for all information regarding costs. " + "Mandatory if cost information is published." + }, ) provides_forecast: bool = Field( - ..., description="Indicates whether the ResourceManager is able to provide PowerForecasts." + ..., + json_schema_extra={ + "description": "Indicates whether the ResourceManager is able to provide PowerForecasts." + }, ) provides_power_measurement_types: list[CommodityQuantity] = Field( ..., - description="Array of all CommodityQuantities that this ResourceManager can provide measurements for.", + json_schema_extra={ + "description": "Array of all CommodityQuantities that this ResourceManager can provide measurements for." + }, ) @@ -662,18 +755,22 @@ class PowerForecastElement(PydanticBaseModel): duration: Duration = Field( ..., - description=( - "Duration of the PowerForecastElement. " - "Defines the time window the power values apply to." - ), + json_schema_extra={ + "description": ( + "Duration of the PowerForecastElement. " + "Defines the time window the power values apply to." + ) + }, ) power_values: list[PowerForecastValue] = Field( ..., min_length=1, - description=( - "The values of power that are expected for the given period. " - "There shall be at least one PowerForecastValue, and at most one per CommodityQuantity." - ), + json_schema_extra={ + "description": ( + "The values of power that are expected for the given period. " + "There shall be at least one PowerForecastValue, and at most one per CommodityQuantity." + ) + }, ) @@ -691,15 +788,20 @@ class PowerForecast(PydanticBaseModel): """ start_time: DateTime = Field( - ..., description="Start time of time period that is covered by the profile." + ..., + json_schema_extra={ + "description": "Start time of time period that is covered by the profile." + }, ) elements: list[PowerForecastElement] = Field( ..., min_length=1, - description=( - "Elements of which this forecast consists. Contains at least one element. " - "Elements shall be placed in chronological order." - ), + json_schema_extra={ + "description": ( + "Elements of which this forecast consists. Contains at least one element. " + "Elements shall be placed in chronological order." + ) + }, ) @@ -723,16 +825,22 @@ class BaseInstruction(PydanticBaseModel, ABC): id: Optional[ID] = Field( default=None, - description=( - "Unique identifier of the instruction in the ResourceManager scope. " - "If not provided and a `resource_id` is passed at instantiation, this will " - "be auto-generated as `{resource_id}@{UUID}`." - ), + json_schema_extra={ + "description": ( + "Unique identifier of the instruction in the ResourceManager scope. " + "If not provided and a `resource_id` is passed at instantiation, this will " + "be auto-generated as `{resource_id}@{UUID}`." + ) + }, + ) + execution_time: DateTime = Field( + ..., json_schema_extra={"description": "Start time of the instruction execution."} ) - execution_time: DateTime = Field(..., description="Start time of the instruction execution.") abnormal_condition: bool = Field( default=False, - description="Indicates if this is an instruction for abnormal conditions. Defaults to False.", + json_schema_extra={ + "description": "Indicates if this is an instruction for abnormal conditions. Defaults to False." + }, ) @model_validator(mode="before") @@ -795,16 +903,25 @@ class PEBCAllowedLimitRange(PydanticBaseModel): """ commodity_quantity: CommodityQuantity = Field( - ..., description="Type of power quantity this range applies to." + ..., json_schema_extra={"description": "Type of power quantity this range applies to."} ) limit_type: PEBCPowerEnvelopeLimitType = Field( - ..., description="Whether this range applies to the upper or lower power envelope limit." + ..., + json_schema_extra={ + "description": "Whether this range applies to the upper or lower power envelope limit." + }, ) range_boundary: NumberRange = Field( - ..., description="Range of values the CEM can choose for the power envelope." + ..., + json_schema_extra={ + "description": "Range of values the CEM can choose for the power envelope." + }, ) abnormal_condition_only: Optional[bool] = Field( - False, description="Indicates if this range can only be used during an abnormal condition." + False, + json_schema_extra={ + "description": "Indicates if this range can only be used during an abnormal condition." + }, ) @@ -817,17 +934,24 @@ class PEBCPowerConstraints(PydanticBaseModel): the controlled device's behavior. """ - id: ID = Field(..., description="Unique identifier of this PowerConstraints set.") - valid_from: DateTime = Field(..., description="Timestamp when these constraints become valid.") + id: ID = Field( + ..., json_schema_extra={"description": "Unique identifier of this PowerConstraints set."} + ) + valid_from: DateTime = Field( + ..., json_schema_extra={"description": "Timestamp when these constraints become valid."} + ) valid_until: Optional[DateTime] = Field( - None, description="Optional end time of validity for these constraints." + None, + json_schema_extra={"description": "Optional end time of validity for these constraints."}, ) consequence_type: PEBCPowerEnvelopeConsequenceType = Field( - ..., description="The type of consequence when limiting power." + ..., json_schema_extra={"description": "The type of consequence when limiting power."} ) allowed_limit_ranges: list[PEBCAllowedLimitRange] = Field( ..., - description="List of allowed power envelope limit ranges. Must contain at least one UPPER_LIMIT and one LOWER_LIMIT.", + json_schema_extra={ + "description": "List of allowed power envelope limit ranges. Must contain at least one UPPER_LIMIT and one LOWER_LIMIT." + }, ) @@ -840,25 +964,37 @@ class PEBCEnergyConstraints(PydanticBaseModel): energy management strategies. """ - id: ID = Field(..., description="Unique identifier of this EnergyConstraints object.") - valid_from: DateTime = Field(..., description="Start time for which this constraint is valid.") - valid_until: DateTime = Field(..., description="End time for which this constraint is valid.") + id: ID = Field( + ..., + json_schema_extra={"description": "Unique identifier of this EnergyConstraints object."}, + ) + valid_from: DateTime = Field( + ..., json_schema_extra={"description": "Start time for which this constraint is valid."} + ) + valid_until: DateTime = Field( + ..., json_schema_extra={"description": "End time for which this constraint is valid."} + ) upper_average_power: float = Field( ..., - description=( - "Maximum average power over the given time period. " - "Used to derive maximum energy content." - ), + json_schema_extra={ + "description": ( + "Maximum average power over the given time period. " + "Used to derive maximum energy content." + ) + }, ) lower_average_power: float = Field( ..., - description=( - "Minimum average power over the given time period. " - "Used to derive minimum energy content." - ), + json_schema_extra={ + "description": ( + "Minimum average power over the given time period. " + "Used to derive minimum energy content." + ) + }, ) commodity_quantity: CommodityQuantity = Field( - ..., description="The commodity or type of power to which this applies." + ..., + json_schema_extra={"description": "The commodity or type of power to which this applies."}, ) @@ -870,20 +1006,26 @@ class PEBCPowerEnvelopeElement(PydanticBaseModel): a time-varying power envelope that constrains device power consumption or production. """ - duration: Duration = Field(..., description="Duration of this power envelope element.") + duration: Duration = Field( + ..., json_schema_extra={"description": "Duration of this power envelope element."} + ) upper_limit: float = Field( ..., - description=( - "Upper power limit for the given commodity_quantity. " - "Shall match PEBC.AllowedLimitRange with limit_type UPPER_LIMIT." - ), + json_schema_extra={ + "description": ( + "Upper power limit for the given commodity_quantity. " + "Shall match PEBC.AllowedLimitRange with limit_type UPPER_LIMIT." + ) + }, ) lower_limit: float = Field( ..., - description=( - "Lower power limit for the given commodity_quantity. " - "Shall match PEBC.AllowedLimitRange with limit_type LOWER_LIMIT." - ), + json_schema_extra={ + "description": ( + "Lower power limit for the given commodity_quantity. " + "Shall match PEBC.AllowedLimitRange with limit_type LOWER_LIMIT." + ) + }, ) @@ -898,20 +1040,24 @@ class PEBCPowerEnvelope(PydanticBaseModel): id: ID = Field( ..., - description=( - "Unique identifier of this PEBC.PowerEnvelope, scoped to the ResourceManager." - ), + json_schema_extra={ + "description": ( + "Unique identifier of this PEBC.PowerEnvelope, scoped to the ResourceManager." + ) + }, ) commodity_quantity: CommodityQuantity = Field( - ..., description="Type of power quantity the envelope applies to." + ..., json_schema_extra={"description": "Type of power quantity the envelope applies to."} ) power_envelope_elements: list[PEBCPowerEnvelopeElement] = Field( ..., min_length=1, - description=( - "Chronologically ordered list of PowerEnvelopeElements. " - "Defines how power should be constrained over time." - ), + json_schema_extra={ + "description": ( + "Chronologically ordered list of PowerEnvelopeElements. " + "Defines how power should be constrained over time." + ) + }, ) @@ -925,13 +1071,17 @@ class PEBCInstruction(BaseInstruction): """ type: Literal["PEBCInstruction"] = Field(default="PEBCInstruction") - power_constraints_id: ID = Field(..., description="ID of the associated PEBC.PowerConstraints.") + power_constraints_id: ID = Field( + ..., json_schema_extra={"description": "ID of the associated PEBC.PowerConstraints."} + ) power_envelopes: list[PEBCPowerEnvelope] = Field( ..., min_length=1, - description=( - "List of PowerEnvelopes to follow. One per CommodityQuantity, max one per type." - ), + json_schema_extra={ + "description": ( + "List of PowerEnvelopes to follow. One per CommodityQuantity, max one per type." + ) + }, ) def duration(self) -> Optional[Duration]: @@ -957,9 +1107,14 @@ class PPBCPowerSequenceElement(PydanticBaseModel): the execution of the sequence. """ - duration: Duration = Field(..., description="Duration of the sequence element.") + duration: Duration = Field( + ..., json_schema_extra={"description": "Duration of the sequence element."} + ) power_values: list[PowerForecastValue] = Field( - ..., description="Forecasted power values for the duration, one per CommodityQuantity." + ..., + json_schema_extra={ + "description": "Forecasted power values for the duration, one per CommodityQuantity." + }, ) @@ -972,19 +1127,33 @@ class PPBCPowerSequence(PydanticBaseModel): supporting flexible power management strategies. """ - id: ID = Field(..., description="Unique identifier of the PowerSequence within its container.") + id: ID = Field( + ..., + json_schema_extra={ + "description": "Unique identifier of the PowerSequence within its container." + }, + ) elements: list[PPBCPowerSequenceElement] = Field( - ..., description="Ordered list of sequence elements representing power behavior." + ..., + json_schema_extra={ + "description": "Ordered list of sequence elements representing power behavior." + }, ) is_interruptible: bool = Field( - ..., description="Indicates whether this sequence can be interrupted." + ..., + json_schema_extra={"description": "Indicates whether this sequence can be interrupted."}, ) max_pause_before: Optional[Duration] = Field( None, - description="Maximum allowed pause before this sequence starts after the previous one.", + json_schema_extra={ + "description": "Maximum allowed pause before this sequence starts after the previous one." + }, ) abnormal_condition_only: bool = Field( - ..., description="True if sequence is only applicable in abnormal conditions." + ..., + json_schema_extra={ + "description": "True if sequence is only applicable in abnormal conditions." + }, ) @@ -999,10 +1168,15 @@ class PPBCPowerSequenceContainer(PydanticBaseModel): id: ID = Field( ..., - description="Unique identifier of the PowerSequenceContainer within its parent PowerProfileDefinition.", + json_schema_extra={ + "description": "Unique identifier of the PowerSequenceContainer within its parent PowerProfileDefinition." + }, ) power_sequences: list[PPBCPowerSequence] = Field( - ..., description="List of alternative PowerSequences. One will be selected by the CEM." + ..., + json_schema_extra={ + "description": "List of alternative PowerSequences. One will be selected by the CEM." + }, ) @@ -1017,17 +1191,25 @@ class PPBCPowerProfileDefinition(PydanticBaseModel): id: ID = Field( ..., - description="Unique identifier of the PowerProfileDefinition within the ResourceManager session.", + json_schema_extra={ + "description": "Unique identifier of the PowerProfileDefinition within the ResourceManager session." + }, ) start_time: DateTime = Field( - ..., description="Earliest possible start time of the first PowerSequence." + ..., + json_schema_extra={ + "description": "Earliest possible start time of the first PowerSequence." + }, ) end_time: DateTime = Field( - ..., description="Latest time the last PowerSequence must be completed." + ..., + json_schema_extra={"description": "Latest time the last PowerSequence must be completed."}, ) power_sequences_containers: list[PPBCPowerSequenceContainer] = Field( ..., - description="List of containers for alternative power sequences, in chronological order.", + json_schema_extra={ + "description": "List of containers for alternative power sequences, in chronological order." + }, ) @@ -1040,18 +1222,24 @@ class PPBCPowerSequenceContainerStatus(PydanticBaseModel): within the broader power profile. """ - power_profile_id: ID = Field(..., description="ID of the related PowerProfileDefinition.") + power_profile_id: ID = Field( + ..., json_schema_extra={"description": "ID of the related PowerProfileDefinition."} + ) sequence_container_id: ID = Field( - ..., description="ID of the PowerSequenceContainer being reported on." + ..., + json_schema_extra={"description": "ID of the PowerSequenceContainer being reported on."}, ) selected_sequence_id: Optional[str] = Field( - None, description="ID of the selected PowerSequence, if any." + None, json_schema_extra={"description": "ID of the selected PowerSequence, if any."} ) progress: Optional[Duration] = Field( - None, description="Elapsed time since the selected sequence started, if applicable." + None, + json_schema_extra={ + "description": "Elapsed time since the selected sequence started, if applicable." + }, ) status: PPBCPowerSequenceStatus = Field( - ..., description="Status of the selected PowerSequence." + ..., json_schema_extra={"description": "Status of the selected PowerSequence."} ) @@ -1066,7 +1254,10 @@ class PPBCPowerProfileStatus(PydanticBaseModel): type: Literal["PPBCPowerProfileStatus"] = Field(default="PPBCPowerProfileStatus") sequence_container_status: list[PPBCPowerSequenceContainerStatus] = Field( - ..., description="Status list for all sequence containers in the PowerProfileDefinition." + ..., + json_schema_extra={ + "description": "Status list for all sequence containers in the PowerProfileDefinition." + }, ) @@ -1082,12 +1273,14 @@ class PPBCScheduleInstruction(BaseInstruction): type: Literal["PPBCScheduleInstruction"] = Field(default="PPBCScheduleInstruction") power_profile_id: ID = Field( - ..., description="ID of the PowerProfileDefinition being scheduled." + ..., json_schema_extra={"description": "ID of the PowerProfileDefinition being scheduled."} ) sequence_container_id: ID = Field( - ..., description="ID of the container with the selected sequence." + ..., json_schema_extra={"description": "ID of the container with the selected sequence."} + ) + power_sequence_id: ID = Field( + ..., json_schema_extra={"description": "ID of the selected PowerSequence."} ) - power_sequence_id: ID = Field(..., description="ID of the selected PowerSequence.") def duration(self) -> Optional[Duration]: # @TODO: PPBCPowerProfileDefinition needed @@ -1107,12 +1300,17 @@ class PPBCStartInterruptionInstruction(BaseInstruction): default="PPBCStartInterruptionInstruction" ) power_profile_id: ID = Field( - ..., description="ID of the PowerProfileDefinition whose sequence is being interrupted." + ..., + json_schema_extra={ + "description": "ID of the PowerProfileDefinition whose sequence is being interrupted." + }, ) sequence_container_id: ID = Field( - ..., description="ID of the container containing the sequence." + ..., json_schema_extra={"description": "ID of the container containing the sequence."} + ) + power_sequence_id: ID = Field( + ..., json_schema_extra={"description": "ID of the PowerSequence to be interrupted."} ) - power_sequence_id: ID = Field(..., description="ID of the PowerSequence to be interrupted.") def duration(self) -> Optional[Duration]: # @TODO: PPBCPowerProfileDefinition needed @@ -1132,13 +1330,19 @@ class PPBCEndInterruptionInstruction(BaseInstruction): default="PPBCEndInterruptionInstruction" ) power_profile_id: ID = Field( - ..., description="ID of the PowerProfileDefinition related to the ended interruption." + ..., + json_schema_extra={ + "description": "ID of the PowerProfileDefinition related to the ended interruption." + }, ) sequence_container_id: ID = Field( - ..., description="ID of the container containing the sequence." + ..., json_schema_extra={"description": "ID of the container containing the sequence."} ) power_sequence_id: ID = Field( - ..., description="ID of the PowerSequence for which the interruption ends." + ..., + json_schema_extra={ + "description": "ID of the PowerSequence for which the interruption ends." + }, ) def duration(self) -> Optional[Duration]: @@ -1157,21 +1361,32 @@ class OMBCOperationMode(PydanticBaseModel): """ id: ID = Field( - ..., description="Unique ID of the OperationMode within the ResourceManager session." + ..., + json_schema_extra={ + "description": "Unique ID of the OperationMode within the ResourceManager session." + }, ) diagnostic_label: Optional[str] = Field( - None, description="Human-readable label for diagnostics (not for HMI)." + None, + json_schema_extra={"description": "Human-readable label for diagnostics (not for HMI)."}, ) power_ranges: list[PowerRange] = Field( ..., - description="List of power consumption or production ranges mapped to operation_mode_factor 0 to 1.", + json_schema_extra={ + "description": "List of power consumption or production ranges mapped to operation_mode_factor 0 to 1." + }, ) running_costs: Optional[NumberRange] = Field( None, - description="Estimated additional costs per second, excluding commodity cost. Represents uncertainty.", + json_schema_extra={ + "description": "Estimated additional costs per second, excluding commodity cost. Represents uncertainty." + }, ) abnormal_condition_only: bool = Field( - ..., description="True if this mode can only be used during an abnormal condition." + ..., + json_schema_extra={ + "description": "True if this mode can only be used during an abnormal condition." + }, ) @@ -1187,19 +1402,25 @@ class OMBCStatus(PydanticBaseModel): type: Literal["OMBCStatus"] = Field(default="OMBCStatus") active_operation_mode_id: ID = Field( - ..., description="ID of the currently active operation mode." + ..., json_schema_extra={"description": "ID of the currently active operation mode."} ) operation_mode_factor: float = Field( ..., ge=0.0, le=1.0, - description="Factor with which the operation mode is configured (between 0 and 1).", + json_schema_extra={ + "description": "Factor with which the operation mode is configured (between 0 and 1)." + }, ) previous_operation_mode_id: Optional[str] = Field( - None, description="ID of the previously active operation mode, if known." + None, + json_schema_extra={"description": "ID of the previously active operation mode, if known."}, ) transition_timestamp: Optional[DateTime] = Field( - None, description="Timestamp of transition to the active operation mode, if applicable." + None, + json_schema_extra={ + "description": "Timestamp of transition to the active operation mode, if applicable." + }, ) @@ -1211,11 +1432,15 @@ class OMBCTimerStatus(PydanticBaseModel): type: Literal["OMBCTimerStatus"] = Field(default="OMBCTimerStatus") - timer_id: ID = Field(..., description="ID of the timer this status refers to.") + timer_id: ID = Field( + ..., json_schema_extra={"description": "ID of the timer this status refers to."} + ) finished_at: DateTime = Field( ..., - description="Indicates when the Timer will be finished. If the DateTime is in the future, the timer is not yet finished. If the DateTime is in the past, the timer is finished. If the timer was never started, the value can be an arbitrary DateTimeStamp in the past.", + json_schema_extra={ + "description": "Indicates when the Timer will be finished. If the DateTime is in the future, the timer is not yet finished. If the DateTime is in the past, the timer is finished. If the timer was never started, the value can be an arbitrary DateTimeStamp in the past." + }, ) @@ -1229,17 +1454,24 @@ class OMBCSystemDefinition(PydanticBaseModel): valid_from: DateTime = Field( ..., - description="Start time from which this system description is valid. Must be in the past or present if immediately applicable.", + json_schema_extra={ + "description": "Start time from which this system description is valid. Must be in the past or present if immediately applicable." + }, ) operation_modes: list[OMBCOperationMode] = Field( ..., - description="List of operation modes available for the CEM to coordinate device behavior.", + json_schema_extra={ + "description": "List of operation modes available for the CEM to coordinate device behavior." + }, ) transitions: list[Transition] = Field( - ..., description="Possible transitions between operation modes." + ..., json_schema_extra={"description": "Possible transitions between operation modes."} ) timers: list[Timer] = Field( - ..., description="Timers specifying constraints for when transitions can occur." + ..., + json_schema_extra={ + "description": "Timers specifying constraints for when transitions can occur." + }, ) @@ -1256,7 +1488,9 @@ class OMBCSystemDescription(OMBCSystemDefinition): status: OMBCStatus = Field( ..., - description="Current status information, including the active operation mode and transition details.", + json_schema_extra={ + "description": "Current status information, including the active operation mode and transition details." + }, ) @@ -1268,12 +1502,16 @@ class OMBCInstruction(BaseInstruction): """ type: Literal["OMBCInstruction"] = Field(default="OMBCInstruction") - operation_mode_id: ID = Field(..., description="ID of the OMBC.OperationMode to activate.") + operation_mode_id: ID = Field( + ..., json_schema_extra={"description": "ID of the OMBC.OperationMode to activate."} + ) operation_mode_factor: float = Field( ..., ge=0.0, le=1.0, - description="Factor with which the operation mode is configured (0 to 1).", + json_schema_extra={ + "description": "Factor with which the operation mode is configured (0 to 1)." + }, ) def duration(self) -> Optional[Duration]: @@ -1291,15 +1529,20 @@ class FRBCOperationModeElement(PydanticBaseModel): its effect on fill rate and associated power consumption/production. """ - fill_level_range: NumberRange = Field(..., description="Fill level range for this element.") + fill_level_range: NumberRange = Field( + ..., json_schema_extra={"description": "Fill level range for this element."} + ) fill_rate: NumberRange = Field( - ..., description="Change in fill level per second for this mode." + ..., json_schema_extra={"description": "Change in fill level per second for this mode."} ) power_ranges: list[PowerRange] = Field( - ..., description="Power produced/consumed per commodity." + ..., json_schema_extra={"description": "Power produced/consumed per commodity."} ) running_costs: Optional[NumberRange] = Field( - None, description="Additional costs per second (excluding commodity cost)." + None, + json_schema_extra={ + "description": "Additional costs per second (excluding commodity cost)." + }, ) @@ -1311,15 +1554,18 @@ class FRBCOperationMode(PydanticBaseModel): distinct way to operate the resource affecting the storage fill level. """ - id: ID = Field(..., description="Unique ID of the operation mode within the actuator.") + id: ID = Field( + ..., + json_schema_extra={"description": "Unique ID of the operation mode within the actuator."}, + ) diagnostic_label: Optional[str] = Field( - None, description="Human-readable label for diagnostics." + None, json_schema_extra={"description": "Human-readable label for diagnostics."} ) elements: list[FRBCOperationModeElement] = Field( - ..., description="Properties of the mode depending on fill level." + ..., json_schema_extra={"description": "Properties of the mode depending on fill level."} ) abnormal_condition_only: bool = Field( - ..., description="True if mode is for abnormal conditions only." + ..., json_schema_extra={"description": "True if mode is for abnormal conditions only."} ) @@ -1332,15 +1578,20 @@ class FRBCActuatorStatus(PydanticBaseModel): type: Literal["FRBCActuatorStatus"] = Field(default="FRBCActuatorStatus") - active_operation_mode_id: ID = Field(..., description="Currently active operation mode ID.") + active_operation_mode_id: ID = Field( + ..., json_schema_extra={"description": "Currently active operation mode ID."} + ) operation_mode_factor: float = Field( - ..., ge=0, le=1, description="Factor with which the mode is configured (0 to 1)." + ..., + ge=0, + le=1, + json_schema_extra={"description": "Factor with which the mode is configured (0 to 1)."}, ) previous_operation_mode_id: Optional[str] = Field( - None, description="Previously active operation mode ID." + None, json_schema_extra={"description": "Previously active operation mode ID."} ) transition_timestamp: Optional[DateTime] = Field( - None, description="Timestamp of the last transition between modes." + None, json_schema_extra={"description": "Timestamp of the last transition between modes."} ) @@ -1351,18 +1602,26 @@ class FRBCActuatorDefinition(PydanticBaseModel): available operation modes, and constraints on transitions between modes. """ - id: ID = Field(..., description="Unique actuator ID within the ResourceManager session.") - diagnostic_label: Optional[str] = Field( - None, description="Human-readable actuator description for diagnostics." + id: ID = Field( + ..., + json_schema_extra={"description": "Unique actuator ID within the ResourceManager session."}, + ) + diagnostic_label: Optional[str] = Field( + None, + json_schema_extra={"description": "Human-readable actuator description for diagnostics."}, + ) + supported_commodities: list[str] = Field( + ..., json_schema_extra={"description": "List of supported commodity IDs."} ) - supported_commodities: list[str] = Field(..., description="List of supported commodity IDs.") operation_modes: list[FRBCOperationMode] = Field( - ..., description="Operation modes provided by this actuator." + ..., json_schema_extra={"description": "Operation modes provided by this actuator."} ) transitions: list[Transition] = Field( - ..., description="Allowed transitions between operation modes." + ..., json_schema_extra={"description": "Allowed transitions between operation modes."} + ) + timers: list[Timer] = Field( + ..., json_schema_extra={"description": "Timers associated with this actuator."} ) - timers: list[Timer] = Field(..., description="Timers associated with this actuator.") class FRBCActuatorDescription(FRBCActuatorDefinition): @@ -1373,7 +1632,9 @@ class FRBCActuatorDescription(FRBCActuatorDefinition): of the actuator. """ - status: FRBCActuatorStatus = Field(..., description="Current status of the actuator.") + status: FRBCActuatorStatus = Field( + ..., json_schema_extra={"description": "Current status of the actuator."} + ) class FRBCEnergyStatus(PydanticBaseModel): @@ -1385,10 +1646,16 @@ class FRBCEnergyStatus(PydanticBaseModel): type: Literal["FRBCEnergyStatus"] = Field(default="FRBCEnergyStatus") import_total: Optional[EnergyMeasurement] = Field( - default=None, description="Total cumulative imported energy from the energy meter start." + default=None, + json_schema_extra={ + "description": "Total cumulative imported energy from the energy meter start." + }, ) export_total: Optional[EnergyMeasurement] = Field( - default=None, description="Total cumulative exported energy from the energy meter start." + default=None, + json_schema_extra={ + "description": "Total cumulative exported energy from the energy meter start." + }, ) @@ -1401,7 +1668,9 @@ class FRBCStorageStatus(PydanticBaseModel): type: Literal["FRBCStorageStatus"] = Field(default="FRBCStorageStatus") - present_fill_level: float = Field(..., description="Current fill level of the storage.") + present_fill_level: float = Field( + ..., json_schema_extra={"description": "Current fill level of the storage."} + ) class FRBCTimerStatus(PydanticBaseModel): @@ -1412,13 +1681,19 @@ class FRBCTimerStatus(PydanticBaseModel): type: Literal["FRBCTimerStatus"] = Field(default="FRBCTimerStatus") - actuator_id: ID = Field(..., description="ID of the actuator the timer belongs to.") + actuator_id: ID = Field( + ..., json_schema_extra={"description": "ID of the actuator the timer belongs to."} + ) - timer_id: ID = Field(..., description="ID of the timer this status refers to.") + timer_id: ID = Field( + ..., json_schema_extra={"description": "ID of the timer this status refers to."} + ) finished_at: DateTime = Field( ..., - description="Indicates when the Timer will be finished. If the DateTime is in the future, the timer is not yet finished. If the DateTime is in the past, the timer is finished. If the timer was never started, the value can be an arbitrary DateTimeStamp in the past.", + json_schema_extra={ + "description": "Indicates when the Timer will be finished. If the DateTime is in the future, the timer is not yet finished. If the DateTime is in the past, the timer is finished. If the timer was never started, the value can be an arbitrary DateTimeStamp in the past." + }, ) @@ -1430,10 +1705,11 @@ class FRBCLeakageBehaviourElement(PydanticBaseModel): """ fill_level_range: NumberRange = Field( - ..., description="Applicable fill level range for this element." + ..., json_schema_extra={"description": "Applicable fill level range for this element."} ) leakage_rate: float = Field( - ..., description="Rate of fill level decrease per second due to leakage." + ..., + json_schema_extra={"description": "Rate of fill level decrease per second due to leakage."}, ) @@ -1444,9 +1720,11 @@ class FRBCLeakageBehaviour(PydanticBaseModel): with leakage rates that may vary based on fill level. """ - valid_from: DateTime = Field(..., description="Start of validity for this leakage behaviour.") + valid_from: DateTime = Field( + ..., json_schema_extra={"description": "Start of validity for this leakage behaviour."} + ) elements: list[FRBCLeakageBehaviourElement] = Field( - ..., description="Contiguous elements modeling leakage." + ..., json_schema_extra={"description": "Contiguous elements modeling leakage."} ) @@ -1457,25 +1735,29 @@ class FRBCUsageForecastElement(PydanticBaseModel): probability ranges to represent uncertainty. """ - duration: Duration = Field(..., description="How long the given usage rate is valid.") + duration: Duration = Field( + ..., json_schema_extra={"description": "How long the given usage rate is valid."} + ) usage_rate_upper_limit: Optional[float] = Field( - None, description="100% probability upper limit." + None, json_schema_extra={"description": "100% probability upper limit."} ) usage_rate_upper_95PPR: Optional[float] = Field( - None, description="95% probability upper limit." + None, json_schema_extra={"description": "95% probability upper limit."} ) usage_rate_upper_68PPR: Optional[float] = Field( - None, description="68% probability upper limit." + None, json_schema_extra={"description": "68% probability upper limit."} + ) + usage_rate_expected: float = Field( + ..., json_schema_extra={"description": "Most likely usage rate."} ) - usage_rate_expected: float = Field(..., description="Most likely usage rate.") usage_rate_lower_68PPR: Optional[float] = Field( - None, description="68% probability lower limit." + None, json_schema_extra={"description": "68% probability lower limit."} ) usage_rate_lower_95PPR: Optional[float] = Field( - None, description="95% probability lower limit." + None, json_schema_extra={"description": "95% probability lower limit."} ) usage_rate_lower_limit: Optional[float] = Field( - None, description="100% probability lower limit." + None, json_schema_extra={"description": "100% probability lower limit."} ) @@ -1486,9 +1768,11 @@ class FRBCUsageForecast(PydanticBaseModel): allowing for planning of optimal resource operation. """ - start_time: DateTime = Field(..., description="Start time of the forecast.") + start_time: DateTime = Field( + ..., json_schema_extra={"description": "Start time of the forecast."} + ) elements: list[FRBCUsageForecastElement] = Field( - ..., description="Chronological forecast profile elements." + ..., json_schema_extra={"description": "Chronological forecast profile elements."} ) @@ -1499,9 +1783,11 @@ class FRBCFillLevelTargetProfileElement(PydanticBaseModel): used to guide resource operation planning. """ - duration: Duration = Field(..., description="Duration this target applies for.") + duration: Duration = Field( + ..., json_schema_extra={"description": "Duration this target applies for."} + ) fill_level_range: NumberRange = Field( - ..., description="Target fill level range for the duration." + ..., json_schema_extra={"description": "Target fill level range for the duration."} ) @@ -1512,9 +1798,11 @@ class FRBCFillLevelTargetProfile(PydanticBaseModel): for the control system to achieve through resource operation. """ - start_time: DateTime = Field(..., description="Start time of the fill level target profile.") + start_time: DateTime = Field( + ..., json_schema_extra={"description": "Start time of the fill level target profile."} + ) elements: list[FRBCFillLevelTargetProfileElement] = Field( - ..., description="Chronological list of target ranges." + ..., json_schema_extra={"description": "Chronological list of target ranges."} ) @@ -1526,16 +1814,16 @@ class FRBCStorageDefinition(PydanticBaseModel): """ diagnostic_label: Optional[str] = Field( - None, description="Diagnostic description of the storage." + None, json_schema_extra={"description": "Diagnostic description of the storage."} ) fill_level_label: Optional[str] = Field( - None, description="Description of fill level units (e.g. °C, %)." + None, json_schema_extra={"description": "Description of fill level units (e.g. °C, %)."} ) fill_level_range: NumberRange = Field( - ..., description="Range in which fill level should remain." + ..., json_schema_extra={"description": "Range in which fill level should remain."} ) leakage_behaviour: Optional[FRBCLeakageBehaviour] = Field( - None, description="Details of buffer leakage behaviour." + None, json_schema_extra={"description": "Details of buffer leakage behaviour."} ) @@ -1546,15 +1834,17 @@ class FRBCStorageDescription(FRBCStorageDefinition): constraints, current status, and behavior characteristics. """ - status: FRBCStorageStatus = Field(..., description="Current storage status.") + status: FRBCStorageStatus = Field( + ..., json_schema_extra={"description": "Current storage status."} + ) provides_leakage_behaviour: bool = Field( - ..., description="True if leakage behaviour can be provided." + ..., json_schema_extra={"description": "True if leakage behaviour can be provided."} ) provides_fill_level_target_profile: bool = Field( - ..., description="True if fill level target profile can be provided." + ..., json_schema_extra={"description": "True if fill level target profile can be provided."} ) provides_usage_forecast: bool = Field( - ..., description="True if usage forecast can be provided." + ..., json_schema_extra={"description": "True if usage forecast can be provided."} ) @@ -1566,10 +1856,17 @@ class FRBCInstruction(BaseInstruction): """ type: Literal["FRBCInstruction"] = Field(default="FRBCInstruction") - actuator_id: ID = Field(..., description="ID of the actuator this instruction belongs to.") - operation_mode_id: str = Field(..., description="ID of the operation mode to activate.") + actuator_id: ID = Field( + ..., json_schema_extra={"description": "ID of the actuator this instruction belongs to."} + ) + operation_mode_id: str = Field( + ..., json_schema_extra={"description": "ID of the operation mode to activate."} + ) operation_mode_factor: float = Field( - ..., ge=0, le=1, description="Factor for the operation mode configuration (0 to 1)." + ..., + ge=0, + le=1, + json_schema_extra={"description": "Factor for the operation mode configuration (0 to 1)."}, ) def duration(self) -> Optional[Duration]: @@ -1584,9 +1881,15 @@ class FRBCSystemDescription(PydanticBaseModel): including actuators and storage. This is the top-level model for FRBC. """ - valid_from: DateTime = Field(..., description="Time this system description becomes valid.") - actuators: list[FRBCActuatorDescription] = Field(..., description="List of all actuators.") - storage: FRBCStorageDescription = Field(..., description="Details of the storage.") + valid_from: DateTime = Field( + ..., json_schema_extra={"description": "Time this system description becomes valid."} + ) + actuators: list[FRBCActuatorDescription] = Field( + ..., json_schema_extra={"description": "List of all actuators."} + ) + storage: FRBCStorageDescription = Field( + ..., json_schema_extra={"description": "Details of the storage."} + ) # Control Types - Demand Driven Based Control (DDBC) @@ -1601,25 +1904,40 @@ class DDBCOperationMode(PydanticBaseModel): """ id: ID = Field( - ..., description="ID of the operation mode. Must be unique within the actuator description." + ..., + json_schema_extra={ + "description": "ID of the operation mode. Must be unique within the actuator description." + }, ) diagnostic_label: Optional[str] = Field( - None, description="Human-readable name/description for diagnostics (not for HMI)." + None, + json_schema_extra={ + "description": "Human-readable name/description for diagnostics (not for HMI)." + }, ) power_ranges: list[PowerRange] = Field( ..., - description="Power ranges associated with this operation mode. At least one per CommodityQuantity.", + json_schema_extra={ + "description": "Power ranges associated with this operation mode. At least one per CommodityQuantity." + }, ) supply_range: NumberRange = Field( - ..., description="Supply rate that can match the demand rate, mapped from factor 0 to 1." + ..., + json_schema_extra={ + "description": "Supply rate that can match the demand rate, mapped from factor 0 to 1." + }, ) running_costs: NumberRange = Field( ..., - description="Additional cost per second (excluding commodity cost). Represents uncertainty, not linked to factor.", + json_schema_extra={ + "description": "Additional cost per second (excluding commodity cost). Represents uncertainty, not linked to factor." + }, ) abnormal_condition_only: Optional[bool] = Field( False, - description="Whether this operation mode may only be used during abnormal conditions.", + json_schema_extra={ + "description": "Whether this operation mode may only be used during abnormal conditions." + }, ) @@ -1632,16 +1950,26 @@ class DDBCActuatorStatus(PydanticBaseModel): type: Literal["DDBCActuatorStatus"] = Field(default="DDBCActuatorStatus") - active_operation_mode_id: ID = Field(..., description="Currently active operation mode ID.") + active_operation_mode_id: ID = Field( + ..., json_schema_extra={"description": "Currently active operation mode ID."} + ) operation_mode_factor: float = Field( - ..., ge=0, le=1, description="Factor with which the operation mode is configured (0 to 1)." + ..., + ge=0, + le=1, + json_schema_extra={ + "description": "Factor with which the operation mode is configured (0 to 1)." + }, ) previous_operation_mode_id: Optional[str] = Field( None, - description="Previously active operation mode ID. Required unless this is the first mode.", + json_schema_extra={ + "description": "Previously active operation mode ID. Required unless this is the first mode." + }, ) transition_timestamp: Optional[DateTime] = Field( - None, description="Timestamp of transition to the active operation mode." + None, + json_schema_extra={"description": "Timestamp of transition to the active operation mode."}, ) @@ -1654,23 +1982,39 @@ class DDBCActuatorDefinition(PydanticBaseModel): id: ID = Field( ..., - description="ID of this actuator. Must be unique in the ResourceManager scope during the session.", + json_schema_extra={ + "description": "ID of this actuator. Must be unique in the ResourceManager scope during the session." + }, ) diagnostic_label: Optional[str] = Field( - None, description="Human-readable name/description for diagnostics (not for HMI)." + None, + json_schema_extra={ + "description": "Human-readable name/description for diagnostics (not for HMI)." + }, ) supported_commodities: list[str] = Field( - ..., description="Commodities supported by this actuator. Must include at least one." + ..., + json_schema_extra={ + "description": "Commodities supported by this actuator. Must include at least one." + }, ) operation_modes: list[DDBCOperationMode] = Field( ..., - description="List of available operation modes for this actuator. Must include at least one.", + json_schema_extra={ + "description": "List of available operation modes for this actuator. Must include at least one." + }, ) transitions: list[Transition] = Field( - ..., description="List of transitions between operation modes. Must include at least one." + ..., + json_schema_extra={ + "description": "List of transitions between operation modes. Must include at least one." + }, ) timers: list[Timer] = Field( - ..., description="List of timers associated with transitions. Can be empty." + ..., + json_schema_extra={ + "description": "List of timers associated with transitions. Can be empty." + }, ) @@ -1682,7 +2026,9 @@ class DDBCActuatorDescription(DDBCActuatorDefinition): its present status. """ - status: DDBCActuatorStatus = Field(..., description="Present status of this actuator.") + status: DDBCActuatorStatus = Field( + ..., json_schema_extra={"description": "Present status of this actuator."} + ) class DDBCSystemDescription(PydanticBaseModel): @@ -1694,18 +2040,27 @@ class DDBCSystemDescription(PydanticBaseModel): valid_from: DateTime = Field( ..., - description="Moment this DDBC.SystemDescription starts to be valid. If immediately valid, it should be now or in the past.", + json_schema_extra={ + "description": "Moment this DDBC.SystemDescription starts to be valid. If immediately valid, it should be now or in the past." + }, ) actuators: list[DDBCActuatorDescription] = Field( ..., - description="List of all available actuators in the system. Shall contain at least one DDBC.ActuatorAggregated.", + json_schema_extra={ + "description": "List of all available actuators in the system. Shall contain at least one DDBC.ActuatorAggregated." + }, ) present_demand_rate: NumberRange = Field( - ..., description="Present demand rate that needs to be satisfied by the system." + ..., + json_schema_extra={ + "description": "Present demand rate that needs to be satisfied by the system." + }, ) provides_average_demand_rate_forecast: bool = Field( ..., - description="Indicates whether a demand rate forecast is provided through DDBC.AverageDemandRateForecast.", + json_schema_extra={ + "description": "Indicates whether a demand rate forecast is provided through DDBC.AverageDemandRateForecast." + }, ) @@ -1717,13 +2072,19 @@ class DDBCInstruction(BaseInstruction): """ type: Literal["DDBCInstruction"] = Field(default="DDBCInstruction") - actuator_id: ID = Field(..., description="ID of the actuator this instruction belongs to.") - operation_mode_id: ID = Field(..., description="ID of the DDBC.OperationMode to apply.") + actuator_id: ID = Field( + ..., json_schema_extra={"description": "ID of the actuator this instruction belongs to."} + ) + operation_mode_id: ID = Field( + ..., json_schema_extra={"description": "ID of the DDBC.OperationMode to apply."} + ) operation_mode_factor: float = Field( ..., ge=0, le=1, - description="Factor with which the operation mode should be applied (0 to 1).", + json_schema_extra={ + "description": "Factor with which the operation mode should be applied (0 to 1)." + }, ) def duration(self) -> Optional[Duration]: @@ -1738,27 +2099,32 @@ class DDBCAverageDemandRateForecastElement(PydanticBaseModel): probability ranges to represent uncertainty. """ - duration: Duration = Field(..., description="Duration of this forecast element.") + duration: Duration = Field( + ..., json_schema_extra={"description": "Duration of this forecast element."} + ) demand_rate_upper_limit: Optional[float] = Field( - None, description="100% upper limit of demand rate range." + None, json_schema_extra={"description": "100% upper limit of demand rate range."} ) demand_rate_upper_95PPR: Optional[float] = Field( - None, description="95% upper limit of demand rate range." + None, json_schema_extra={"description": "95% upper limit of demand rate range."} ) demand_rate_upper_68PPR: Optional[float] = Field( - None, description="68% upper limit of demand rate range." + None, json_schema_extra={"description": "68% upper limit of demand rate range."} ) demand_rate_expected: float = Field( - ..., description="Expected demand rate (fill level increase/decrease per second)." + ..., + json_schema_extra={ + "description": "Expected demand rate (fill level increase/decrease per second)." + }, ) demand_rate_lower_68PPR: Optional[float] = Field( - None, description="68% lower limit of demand rate range." + None, json_schema_extra={"description": "68% lower limit of demand rate range."} ) demand_rate_lower_95PPR: Optional[float] = Field( - None, description="95% lower limit of demand rate range." + None, json_schema_extra={"description": "95% lower limit of demand rate range."} ) demand_rate_lower_limit: Optional[float] = Field( - None, description="100% lower limit of demand rate range." + None, json_schema_extra={"description": "100% lower limit of demand rate range."} ) @@ -1770,10 +2136,13 @@ class DDBCAverageDemandRateForecast(PydanticBaseModel): """ start_time: DateTime = Field( - ..., description="Start time of the average demand rate forecast profile." + ..., + json_schema_extra={ + "description": "Start time of the average demand rate forecast profile." + }, ) elements: list[DDBCAverageDemandRateForecastElement] = Field( - ..., description="List of forecast elements in chronological order." + ..., json_schema_extra={"description": "List of forecast elements in chronological order."} ) @@ -1826,23 +2195,30 @@ class EnergyManagementPlan(PydanticBaseModel): comment (Optional[str]): Optional comment or annotation for the plan. """ - id: ID = Field(..., description="Unique ID for the energy management plan.") - generated_at: DateTime = Field(..., description="Timestamp when the plan was generated.") + id: ID = Field( + ..., json_schema_extra={"description": "Unique ID for the energy management plan."} + ) + generated_at: DateTime = Field( + ..., json_schema_extra={"description": "Timestamp when the plan was generated."} + ) valid_from: Optional[DateTime] = Field( - default=None, description="Earliest start time of any instruction." + default=None, json_schema_extra={"description": "Earliest start time of any instruction."} ) valid_until: Optional[DateTime] = Field( default=None, - description=( - "Latest end time across all instructions with finite duration; " - "None if all instructions have infinite duration." - ), + json_schema_extra={ + "description": ( + "Latest end time across all instructions with finite duration; " + "None if all instructions have infinite duration." + ) + }, ) instructions: list[EnergyManagementInstruction] = Field( - ..., description="List of control instructions for the plan." + ..., json_schema_extra={"description": "List of control instructions for the plan."} ) comment: Optional[str] = Field( - default=None, description="Optional comment or annotation for the plan." + default=None, + json_schema_extra={"description": "Optional comment or annotation for the plan."}, ) def _update_time_range(self) -> None: diff --git a/src/akkudoktoreos/core/emsettings.py b/src/akkudoktoreos/core/emsettings.py index 99b12bc..b22e2b6 100644 --- a/src/akkudoktoreos/core/emsettings.py +++ b/src/akkudoktoreos/core/emsettings.py @@ -24,17 +24,23 @@ class EnergyManagementCommonSettings(SettingsBaseModel): startup_delay: float = Field( default=5, ge=1, - description="Startup delay in seconds for EOS energy management runs.", + json_schema_extra={ + "description": "Startup delay in seconds for EOS energy management runs." + }, ) interval: Optional[float] = Field( default=None, - description="Intervall in seconds between EOS energy management runs.", - examples=["300"], + json_schema_extra={ + "description": "Intervall in seconds between EOS energy management runs.", + "examples": ["300"], + }, ) mode: Optional[EnergyManagementMode] = Field( default=None, - description="Energy management mode [OPTIMIZATION | PREDICTION].", - examples=["OPTIMIZATION", "PREDICTION"], + json_schema_extra={ + "description": "Energy management mode [OPTIMIZATION | PREDICTION].", + "examples": ["OPTIMIZATION", "PREDICTION"], + }, ) diff --git a/src/akkudoktoreos/core/logsettings.py b/src/akkudoktoreos/core/logsettings.py index 0eeab29..4a32a8d 100644 --- a/src/akkudoktoreos/core/logsettings.py +++ b/src/akkudoktoreos/core/logsettings.py @@ -17,14 +17,18 @@ class LoggingCommonSettings(SettingsBaseModel): console_level: Optional[str] = Field( default=None, - description="Logging level when logging to console.", - examples=LOGGING_LEVELS, + json_schema_extra={ + "description": "Logging level when logging to console.", + "examples": LOGGING_LEVELS, + }, ) file_level: Optional[str] = Field( default=None, - description="Logging level when logging to file.", - examples=LOGGING_LEVELS, + json_schema_extra={ + "description": "Logging level when logging to file.", + "examples": LOGGING_LEVELS, + }, ) @computed_field # type: ignore[prop-decorator] diff --git a/src/akkudoktoreos/core/pydantic.py b/src/akkudoktoreos/core/pydantic.py index 5b4511c..337ef1c 100644 --- a/src/akkudoktoreos/core/pydantic.py +++ b/src/akkudoktoreos/core/pydantic.py @@ -43,6 +43,7 @@ from pydantic import ( ValidationInfo, field_validator, ) +from pydantic.fields import ComputedFieldInfo, FieldInfo from akkudoktoreos.utils.datetimeutil import DateTime, to_datetime, to_duration @@ -720,6 +721,146 @@ class PydanticBaseModel(PydanticModelNestedValueMixin, BaseModel): data = json.loads(json_str) return cls.model_validate(data) + @classmethod + def _field_extra_dict( + cls, + model_field: Union[FieldInfo, ComputedFieldInfo], + ) -> Dict[str, Any]: + """Return the ``json_schema_extra`` dictionary for a given model field. + + This method provides a safe and unified way to access the + ``json_schema_extra`` metadata associated with a Pydantic field + definition. It supports both standard fields defined via + ``Field(...)`` and computed fields, and gracefully handles + cases where ``json_schema_extra`` is not present. + + Args: + model_field (Union[FieldInfo, ComputedFieldInfo]): + The Pydantic field object from which to extract + ``json_schema_extra`` metadata. This can be obtained + from ``model.model_fields[field_name]`` or + ``model.model_computed_fields[field_name]``. + + Returns: + Dict[str, Any]: + A dictionary containing the field’s ``json_schema_extra`` + metadata. If no metadata is available, an empty dictionary + is returned. + + Raises: + None: + This method does not raise. Missing metadata is handled + gracefully by returning an empty dictionary. + + Examples: + >>> class User(Base): + ... name: str = Field( + ... json_schema_extra={"description": "User name"} + ... ) + ... + >>> field = User.model_fields["name"] + >>> User.get_field_extra_dict(field) + {'description': 'User name'} + + >>> missing = User.model_fields.get("unknown", None) + >>> User.get_field_extra_dict(missing) if missing else {} + {} + """ + if model_field is None: + return {} + + # Pydantic v2 primary location + extra = getattr(model_field, "json_schema_extra", None) + if isinstance(extra, dict): + return extra + + # Pydantic v1 compatibility fallback + fi = getattr(model_field, "field_info", None) + if fi is not None: + extra = getattr(fi, "json_schema_extra", None) + if isinstance(extra, dict): + return extra + + return {} + + @classmethod + def field_description(cls, field_name: str) -> Optional[str]: + """Return the description metadata of a model field, if available. + + This method retrieves the `Field` specification from the model's + `model_fields` registry and extracts its description from the field's + `json_schema_extra` / `extra` metadata (as provided by + `_field_extra_dict`). If the field does not exist or no description is + present, ``None`` is returned. + + Args: + field_name (str): + Name of the field whose description should be returned. + + Returns: + Optional[str]: + The textual description if present, otherwise ``None``. + """ + field = cls.model_fields.get(field_name) + if not field: + return None + extra = cls._field_extra_dict(field) + if "description" in extra: + return str(extra["description"]) + return None + + @classmethod + def field_deprecated(cls, field_name: str) -> Optional[str]: + """Return the deprecated metadata of a model field, if available. + + This method retrieves the `Field` specification from the model's + `model_fields` registry and extracts its description from the field's + `json_schema_extra` / `extra` metadata (as provided by + `_field_extra_dict`). If the field does not exist or no description is + present, ``None`` is returned. + + Args: + field_name (str): + Name of the field whose deprecated info should be returned. + + Returns: + Optional[str]: + The textual deprecated info if present, otherwise ``None``. + """ + field = cls.model_fields.get(field_name) + if not field: + return None + extra = cls._field_extra_dict(field) + if "deprecated" in extra: + return str(extra["deprecated"]) + return None + + @classmethod + def field_examples(cls, field_name: str) -> Optional[list[Any]]: + """Return the examples metadata of a model field, if available. + + This method retrieves the `Field` specification from the model's + `model_fields` registry and extracts its description from the field's + `json_schema_extra` / `extra` metadata (as provided by + `_field_extra_dict`). If the field does not exist or no description is + present, ``None`` is returned. + + Args: + field_name (str): + Name of the field whose examples should be returned. + + Returns: + Optional[list[Any]]: + The examples if present, otherwise ``None``. + """ + field = cls.model_fields.get(field_name) + if not field: + return None + extra = cls._field_extra_dict(field) + if "examples" in extra: + return extra["examples"] + return None + class PydanticDateTimeData(RootModel): """Pydantic model for time series data with consistent value lengths. @@ -795,9 +936,12 @@ class PydanticDateTimeDataFrame(PydanticBaseModel): data: Dict[str, Dict[str, Any]] dtypes: Dict[str, str] = Field(default_factory=dict) - tz: Optional[str] = Field(default=None, description="Timezone for datetime values") + tz: Optional[str] = Field( + default=None, json_schema_extra={"description": "Timezone for datetime values"} + ) datetime_columns: list[str] = Field( - default_factory=lambda: ["date_time"], description="Columns to be treated as datetime" + default_factory=lambda: ["date_time"], + json_schema_extra={"description": "Columns to be treated as datetime"}, ) @field_validator("tz") diff --git a/src/akkudoktoreos/devices/devices.py b/src/akkudoktoreos/devices/devices.py index 37f5729..43cdd64 100644 --- a/src/akkudoktoreos/devices/devices.py +++ b/src/akkudoktoreos/devices/devices.py @@ -25,74 +25,81 @@ class BatteriesCommonSettings(DevicesBaseSettings): """Battery devices base settings.""" capacity_wh: int = Field( - default=8000, - gt=0, - description="Capacity [Wh].", - examples=[8000], + default=8000, gt=0, json_schema_extra={"description": "Capacity [Wh].", "examples": [8000]} ) charging_efficiency: float = Field( default=0.88, gt=0, le=1, - description="Charging efficiency [0.01 ... 1.00].", - examples=[0.88], + json_schema_extra={ + "description": "Charging efficiency [0.01 ... 1.00].", + "examples": [0.88], + }, ) discharging_efficiency: float = Field( default=0.88, gt=0, le=1, - description="Discharge efficiency [0.01 ... 1.00].", - examples=[0.88], + json_schema_extra={ + "description": "Discharge efficiency [0.01 ... 1.00].", + "examples": [0.88], + }, ) levelized_cost_of_storage_kwh: float = Field( default=0.0, - description="Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh].", - examples=[0.12], + json_schema_extra={ + "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh].", + "examples": [0.12], + }, ) max_charge_power_w: Optional[float] = Field( default=5000, gt=0, - description="Maximum charging power [W].", - examples=[5000], + json_schema_extra={"description": "Maximum charging power [W].", "examples": [5000]}, ) min_charge_power_w: Optional[float] = Field( default=50, gt=0, - description="Minimum charging power [W].", - examples=[50], + json_schema_extra={"description": "Minimum charging power [W].", "examples": [50]}, ) charge_rates: Optional[NDArray[Shape["*"], float]] = Field( default=BATTERY_DEFAULT_CHARGE_RATES, - description=( - "Charge rates as factor of maximum charging power [0.00 ... 1.00]. " - "None triggers fallback to default charge-rates." - ), - examples=[[0.0, 0.25, 0.5, 0.75, 1.0], None], + json_schema_extra={ + "description": ( + "Charge rates as factor of maximum charging power [0.00 ... 1.00]. " + "None triggers fallback to default charge-rates." + ), + "examples": [[0.0, 0.25, 0.5, 0.75, 1.0], None], + }, ) min_soc_percentage: int = Field( default=0, ge=0, le=100, - description=( - "Minimum state of charge (SOC) as percentage of capacity [%]. " - "This is the target SoC for charging" - ), - examples=[10], + json_schema_extra={ + "description": ( + "Minimum state of charge (SOC) as percentage of capacity [%]. " + "This is the target SoC for charging" + ), + "examples": [10], + }, ) max_soc_percentage: int = Field( default=100, ge=0, le=100, - description="Maximum state of charge (SOC) as percentage of capacity [%].", - examples=[100], + json_schema_extra={ + "description": "Maximum state of charge (SOC) as percentage of capacity [%].", + "examples": [100], + }, ) @field_validator("charge_rates", mode="before") @@ -178,14 +185,15 @@ class InverterCommonSettings(DevicesBaseSettings): max_power_w: Optional[float] = Field( default=None, gt=0, - description="Maximum power [W].", - examples=[10000], + json_schema_extra={"description": "Maximum power [W].", "examples": [10000]}, ) battery_id: Optional[str] = Field( default=None, - description="ID of battery controlled by this inverter.", - examples=[None, "battery1"], + json_schema_extra={ + "description": "ID of battery controlled by this inverter.", + "examples": [None, "battery1"], + }, ) @computed_field # type: ignore[prop-decorator] @@ -200,28 +208,27 @@ class HomeApplianceCommonSettings(DevicesBaseSettings): """Home Appliance devices base settings.""" consumption_wh: int = Field( - gt=0, - description="Energy consumption [Wh].", - examples=[2000], + gt=0, json_schema_extra={"description": "Energy consumption [Wh].", "examples": [2000]} ) duration_h: int = Field( gt=0, le=24, - description="Usage duration in hours [0 ... 24].", - examples=[1], + json_schema_extra={"description": "Usage duration in hours [0 ... 24].", "examples": [1]}, ) time_windows: Optional[TimeWindowSequence] = Field( default=None, - description="Sequence of allowed time windows. Defaults to optimization general time window.", - examples=[ - { - "windows": [ - {"start_time": "10:00", "duration": "2 hours"}, - ], - }, - ], + json_schema_extra={ + "description": "Sequence of allowed time windows. Defaults to optimization general time window.", + "examples": [ + { + "windows": [ + {"start_time": "10:00", "duration": "2 hours"}, + ], + }, + ], + }, ) @computed_field # type: ignore[prop-decorator] @@ -237,50 +244,62 @@ class DevicesCommonSettings(SettingsBaseModel): batteries: Optional[list[BatteriesCommonSettings]] = Field( default=None, - description="List of battery devices", - examples=[[{"device_id": "battery1", "capacity_wh": 8000}]], + json_schema_extra={ + "description": "List of battery devices", + "examples": [[{"device_id": "battery1", "capacity_wh": 8000}]], + }, ) max_batteries: Optional[int] = Field( default=None, ge=0, - description="Maximum number of batteries that can be set", - examples=[1, 2], + json_schema_extra={ + "description": "Maximum number of batteries that can be set", + "examples": [1, 2], + }, ) electric_vehicles: Optional[list[BatteriesCommonSettings]] = Field( default=None, - description="List of electric vehicle devices", - examples=[[{"device_id": "battery1", "capacity_wh": 8000}]], + json_schema_extra={ + "description": "List of electric vehicle devices", + "examples": [[{"device_id": "battery1", "capacity_wh": 8000}]], + }, ) max_electric_vehicles: Optional[int] = Field( default=None, ge=0, - description="Maximum number of electric vehicles that can be set", - examples=[1, 2], + json_schema_extra={ + "description": "Maximum number of electric vehicles that can be set", + "examples": [1, 2], + }, ) inverters: Optional[list[InverterCommonSettings]] = Field( - default=None, description="List of inverters", examples=[[]] + default=None, json_schema_extra={"description": "List of inverters", "examples": [[]]} ) max_inverters: Optional[int] = Field( default=None, ge=0, - description="Maximum number of inverters that can be set", - examples=[1, 2], + json_schema_extra={ + "description": "Maximum number of inverters that can be set", + "examples": [1, 2], + }, ) home_appliances: Optional[list[HomeApplianceCommonSettings]] = Field( - default=None, description="List of home appliances", examples=[[]] + default=None, json_schema_extra={"description": "List of home appliances", "examples": [[]]} ) max_home_appliances: Optional[int] = Field( default=None, ge=0, - description="Maximum number of home_appliances that can be set", - examples=[1, 2], + json_schema_extra={ + "description": "Maximum number of home_appliances that can be set", + "examples": [1, 2], + }, ) @computed_field # type: ignore[prop-decorator] @@ -336,13 +355,17 @@ class ResourceRegistry(SingletonMixin, ConfigMixin, PydanticBaseModel): latest: dict[ResourceKey, ResourceStatus] = Field( default_factory=dict, - description="Latest resource status that was reported per resource key.", - example=[], + json_schema_extra={ + "description": "Latest resource status that was reported per resource key.", + "example": [], + }, ) history: dict[ResourceKey, list[tuple[DateTime, ResourceStatus]]] = Field( default_factory=dict, - description="History of resource stati that were reported per resource key.", - example=[], + json_schema_extra={ + "description": "History of resource stati that were reported per resource key.", + "example": [], + }, ) @model_validator(mode="after") diff --git a/src/akkudoktoreos/devices/devicesabc.py b/src/akkudoktoreos/devices/devicesabc.py index f9a6b91..94baa62 100644 --- a/src/akkudoktoreos/devices/devicesabc.py +++ b/src/akkudoktoreos/devices/devicesabc.py @@ -12,8 +12,10 @@ class DevicesBaseSettings(SettingsBaseModel): device_id: str = Field( default="", - description="ID of device", - examples=["battery1", "ev1", "inverter1", "dishwasher"], + json_schema_extra={ + "description": "ID of device", + "examples": ["battery1", "ev1", "inverter1", "dishwasher"], + }, ) diff --git a/src/akkudoktoreos/measurement/measurement.py b/src/akkudoktoreos/measurement/measurement.py index 70dc543..3ad2442 100644 --- a/src/akkudoktoreos/measurement/measurement.py +++ b/src/akkudoktoreos/measurement/measurement.py @@ -24,26 +24,34 @@ class MeasurementCommonSettings(SettingsBaseModel): load_emr_keys: Optional[list[str]] = Field( default=None, - description="The keys of the measurements that are energy meter readings of a load [kWh].", - examples=[["load0_emr"]], + json_schema_extra={ + "description": "The keys of the measurements that are energy meter readings of a load [kWh].", + "examples": [["load0_emr"]], + }, ) grid_export_emr_keys: Optional[list[str]] = Field( default=None, - description="The keys of the measurements that are energy meter readings of energy export to grid [kWh].", - examples=[["grid_export_emr"]], + json_schema_extra={ + "description": "The keys of the measurements that are energy meter readings of energy export to grid [kWh].", + "examples": [["grid_export_emr"]], + }, ) grid_import_emr_keys: Optional[list[str]] = Field( default=None, - description="The keys of the measurements that are energy meter readings of energy import from grid [kWh].", - examples=[["grid_import_emr"]], + json_schema_extra={ + "description": "The keys of the measurements that are energy meter readings of energy import from grid [kWh].", + "examples": [["grid_import_emr"]], + }, ) pv_production_emr_keys: Optional[list[str]] = Field( default=None, - description="The keys of the measurements that are PV production energy meter readings [kWh].", - examples=[["pv1_emr"]], + json_schema_extra={ + "description": "The keys of the measurements that are PV production energy meter readings [kWh].", + "examples": [["pv1_emr"]], + }, ) ## Computed fields @@ -78,7 +86,7 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence): """ records: list[MeasurementDataRecord] = Field( - default_factory=list, description="list of measurement data records" + default_factory=list, json_schema_extra={"description": "list of measurement data records"} ) def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/src/akkudoktoreos/optimization/genetic/genetic.py b/src/akkudoktoreos/optimization/genetic/genetic.py index a759c43..b854626 100644 --- a/src/akkudoktoreos/optimization/genetic/genetic.py +++ b/src/akkudoktoreos/optimization/genetic/genetic.py @@ -34,50 +34,74 @@ class GeneticSimulation(PydanticBaseModel): ) start_hour: int = Field( - default=0, ge=0, le=23, description="Starting hour on day for optimizations." + default=0, + ge=0, + le=23, + json_schema_extra={"description": "Starting hour on day for optimizations."}, ) optimization_hours: Optional[int] = Field( - default=24, ge=0, description="Number of hours into the future for optimizations." + default=24, + ge=0, + json_schema_extra={"description": "Number of hours into the future for optimizations."}, ) prediction_hours: Optional[int] = Field( - default=48, ge=0, description="Number of hours into the future for predictions" + default=48, + ge=0, + json_schema_extra={"description": "Number of hours into the future for predictions"}, ) load_energy_array: Optional[NDArray[Shape["*"], float]] = Field( default=None, - description="An array of floats representing the total load (consumption) in watts for different time intervals.", + json_schema_extra={ + "description": "An array of floats representing the total load (consumption) in watts for different time intervals." + }, ) pv_prediction_wh: Optional[NDArray[Shape["*"], float]] = Field( default=None, - description="An array of floats representing the forecasted photovoltaic output in watts for different time intervals.", + json_schema_extra={ + "description": "An array of floats representing the forecasted photovoltaic output in watts for different time intervals." + }, ) elect_price_hourly: Optional[NDArray[Shape["*"], float]] = Field( default=None, - description="An array of floats representing the electricity price in euros per watt-hour for different time intervals.", + json_schema_extra={ + "description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals." + }, ) elect_revenue_per_hour_arr: Optional[NDArray[Shape["*"], float]] = Field( default=None, - description="An array of floats representing the feed-in compensation in euros per watt-hour.", + json_schema_extra={ + "description": "An array of floats representing the feed-in compensation in euros per watt-hour." + }, ) - battery: Optional[Battery] = Field(default=None, description="TBD.") - ev: Optional[Battery] = Field(default=None, description="TBD.") - home_appliance: Optional[HomeAppliance] = Field(default=None, description="TBD.") - inverter: Optional[Inverter] = Field(default=None, description="TBD.") + battery: Optional[Battery] = Field(default=None, json_schema_extra={"description": "TBD."}) + ev: Optional[Battery] = Field(default=None, json_schema_extra={"description": "TBD."}) + home_appliance: Optional[HomeAppliance] = Field( + default=None, json_schema_extra={"description": "TBD."} + ) + inverter: Optional[Inverter] = Field(default=None, json_schema_extra={"description": "TBD."}) - ac_charge_hours: Optional[NDArray[Shape["*"], float]] = Field(default=None, description="TBD") - dc_charge_hours: Optional[NDArray[Shape["*"], float]] = Field(default=None, description="TBD") + ac_charge_hours: Optional[NDArray[Shape["*"], float]] = Field( + default=None, json_schema_extra={"description": "TBD"} + ) + dc_charge_hours: Optional[NDArray[Shape["*"], float]] = Field( + default=None, json_schema_extra={"description": "TBD"} + ) bat_discharge_hours: Optional[NDArray[Shape["*"], float]] = Field( - default=None, description="TBD" + default=None, json_schema_extra={"description": "TBD"} + ) + ev_charge_hours: Optional[NDArray[Shape["*"], float]] = Field( + default=None, json_schema_extra={"description": "TBD"} ) - ev_charge_hours: Optional[NDArray[Shape["*"], float]] = Field(default=None, description="TBD") ev_discharge_hours: Optional[NDArray[Shape["*"], float]] = Field( - default=None, description="TBD" + default=None, json_schema_extra={"description": "TBD"} ) home_appliance_start_hour: Optional[int] = Field( - default=None, description="Home appliance start hour - None denotes no start." + default=None, + json_schema_extra={"description": "Home appliance start hour - None denotes no start."}, ) def prepare( diff --git a/src/akkudoktoreos/optimization/genetic/geneticdevices.py b/src/akkudoktoreos/optimization/genetic/geneticdevices.py index 13ccf2a..3fe976b 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticdevices.py +++ b/src/akkudoktoreos/optimization/genetic/geneticdevices.py @@ -9,27 +9,27 @@ from akkudoktoreos.utils.datetimeutil import TimeWindowSequence class DeviceParameters(GeneticParametersBaseModel): - device_id: str = Field(description="ID of device", examples="device1") + device_id: str = Field(json_schema_extra={"description": "ID of device", "examples": "device1"}) hours: Optional[int] = Field( default=None, gt=0, - description="Number of prediction hours. Defaults to global config prediction hours.", - examples=[None], + json_schema_extra={ + "description": "Number of prediction hours. Defaults to global config prediction hours.", + "examples": [None], + }, ) def max_charging_power_field(description: Optional[str] = None) -> float: if description is None: description = "Maximum charging power in watts." - return Field( - default=5000, - gt=0, - description=description, - ) + return Field(default=5000, gt=0, json_schema_extra={"description": description}) def initial_soc_percentage_field(description: str) -> int: - return Field(default=0, ge=0, le=100, description=description, examples=[42]) + return Field( + default=0, ge=0, le=100, json_schema_extra={"description": description, "examples": [42]} + ) def discharging_efficiency_field(default_value: float) -> float: @@ -37,24 +37,32 @@ def discharging_efficiency_field(default_value: float) -> float: default=default_value, gt=0, le=1, - description="A float representing the discharge efficiency of the battery.", + json_schema_extra={ + "description": "A float representing the discharge efficiency of the battery." + }, ) class BaseBatteryParameters(DeviceParameters): """Battery Device Simulation Configuration.""" - device_id: str = Field(description="ID of battery", examples=["battery1"]) + device_id: str = Field( + json_schema_extra={"description": "ID of battery", "examples": ["battery1"]} + ) capacity_wh: int = Field( gt=0, - description="An integer representing the capacity of the battery in watt-hours.", - examples=[8000], + json_schema_extra={ + "description": "An integer representing the capacity of the battery in watt-hours.", + "examples": [8000], + }, ) charging_efficiency: float = Field( default=0.88, gt=0, le=1, - description="A float representing the charging efficiency of the battery.", + json_schema_extra={ + "description": "A float representing the charging efficiency of the battery." + }, ) discharging_efficiency: float = discharging_efficiency_field(0.88) max_charge_power_w: Optional[float] = max_charging_power_field() @@ -65,19 +73,25 @@ class BaseBatteryParameters(DeviceParameters): default=0, ge=0, le=100, - description="An integer representing the minimum state of charge (SOC) of the battery in percentage.", - examples=[10], + json_schema_extra={ + "description": "An integer representing the minimum state of charge (SOC) of the battery in percentage.", + "examples": [10], + }, ) max_soc_percentage: int = Field( default=100, ge=0, le=100, - description="An integer representing the maximum state of charge (SOC) of the battery in percentage.", + json_schema_extra={ + "description": "An integer representing the maximum state of charge (SOC) of the battery in percentage." + }, ) charge_rates: Optional[list[float]] = Field( default=None, - description="Charge rates as factor of maximum charging power [0.00 ... 1.00]. None denotes all charge rates are available.", - examples=[[0.0, 0.25, 0.5, 0.75, 1.0], None], + json_schema_extra={ + "description": "Charge rates as factor of maximum charging power [0.00 ... 1.00]. None denotes all charge rates are available.", + "examples": [[0.0, 0.25, 0.5, 0.75, 1.0], None], + }, ) @@ -90,7 +104,9 @@ class SolarPanelBatteryParameters(BaseBatteryParameters): class ElectricVehicleParameters(BaseBatteryParameters): """Battery Electric Vehicle Device Simulation Configuration.""" - device_id: str = Field(description="ID of electric vehicle", examples=["ev1"]) + device_id: str = Field( + json_schema_extra={"description": "ID of electric vehicle", "examples": ["ev1"]} + ) discharging_efficiency: float = discharging_efficiency_field(1.0) initial_soc_percentage: int = initial_soc_percentage_field( "An integer representing the current state of charge (SOC) of the battery in percentage." @@ -100,33 +116,44 @@ class ElectricVehicleParameters(BaseBatteryParameters): class HomeApplianceParameters(DeviceParameters): """Home Appliance Device Simulation Configuration.""" - device_id: str = Field(description="ID of home appliance", examples=["dishwasher"]) + device_id: str = Field( + json_schema_extra={"description": "ID of home appliance", "examples": ["dishwasher"]} + ) consumption_wh: int = Field( gt=0, - description="An integer representing the energy consumption of a household device in watt-hours.", - examples=[2000], + json_schema_extra={ + "description": "An integer representing the energy consumption of a household device in watt-hours.", + "examples": [2000], + }, ) duration_h: int = Field( gt=0, - description="An integer representing the usage duration of a household device in hours.", - examples=[3], + json_schema_extra={ + "description": "An integer representing the usage duration of a household device in hours.", + "examples": [3], + }, ) time_windows: Optional[TimeWindowSequence] = Field( default=None, - description="List of allowed time windows. Defaults to optimization general time window.", - examples=[ - [ - {"start_time": "10:00", "duration": "2 hours"}, + json_schema_extra={ + "description": "List of allowed time windows. Defaults to optimization general time window.", + "examples": [ + [ + {"start_time": "10:00", "duration": "2 hours"}, + ], ], - ], + }, ) class InverterParameters(DeviceParameters): """Inverter Device Simulation Configuration.""" - device_id: str = Field(description="ID of inverter", examples=["inverter1"]) - max_power_wh: float = Field(gt=0, examples=[10000]) - battery_id: Optional[str] = Field( - default=None, description="ID of battery", examples=[None, "battery1"] + device_id: str = Field( + json_schema_extra={"description": "ID of inverter", "examples": ["inverter1"]} + ) + max_power_wh: float = Field(gt=0, json_schema_extra={"examples": [10000]}) + battery_id: Optional[str] = Field( + default=None, + json_schema_extra={"description": "ID of battery", "examples": [None, "battery1"]}, ) diff --git a/src/akkudoktoreos/optimization/genetic/geneticparams.py b/src/akkudoktoreos/optimization/genetic/geneticparams.py index bc8103a..b1da350 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticparams.py +++ b/src/akkudoktoreos/optimization/genetic/geneticparams.py @@ -37,19 +37,29 @@ class GeneticEnergyManagementParameters(GeneticParametersBaseModel): """Encapsulates energy-related forecasts and costs used in GENETIC optimization.""" pv_prognose_wh: list[float] = Field( - description="An array of floats representing the forecasted photovoltaic output in watts for different time intervals." + json_schema_extra={ + "description": "An array of floats representing the forecasted photovoltaic output in watts for different time intervals." + } ) strompreis_euro_pro_wh: list[float] = Field( - description="An array of floats representing the electricity price in euros per watt-hour for different time intervals." + json_schema_extra={ + "description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals." + } ) einspeiseverguetung_euro_pro_wh: Union[list[float], float] = Field( - description="A float or array of floats representing the feed-in compensation in euros per watt-hour." + json_schema_extra={ + "description": "A float or array of floats representing the feed-in compensation in euros per watt-hour." + } ) preis_euro_pro_wh_akku: float = Field( - description="A float representing the cost of battery energy per watt-hour." + json_schema_extra={ + "description": "A float representing the cost of battery energy per watt-hour." + } ) gesamtlast: list[float] = Field( - description="An array of floats representing the total load (consumption) in watts for different time intervals." + json_schema_extra={ + "description": "An array of floats representing the total load (consumption) in watts for different time intervals." + } ) @model_validator(mode="after") @@ -93,10 +103,15 @@ class GeneticOptimizationParameters( dishwasher: Optional[HomeApplianceParameters] = None temperature_forecast: Optional[list[Optional[float]]] = Field( default=None, - description="An array of floats representing the temperature forecast in degrees Celsius for different time intervals.", + json_schema_extra={ + "description": "An array of floats representing the temperature forecast in degrees Celsius for different time intervals." + }, ) start_solution: Optional[list[float]] = Field( - default=None, description="Can be `null` or contain a previous solution (if available)." + default=None, + json_schema_extra={ + "description": "Can be `null` or contain a previous solution (if available)." + }, ) @model_validator(mode="after") diff --git a/src/akkudoktoreos/optimization/genetic/geneticsolution.py b/src/akkudoktoreos/optimization/genetic/geneticsolution.py index fe2d665..78496bd 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticsolution.py +++ b/src/akkudoktoreos/optimization/genetic/geneticsolution.py @@ -28,29 +28,52 @@ from akkudoktoreos.utils.utils import NumpyEncoder class DeviceOptimizeResult(GeneticParametersBaseModel): - device_id: str = Field(description="ID of device", examples=["device1"]) - hours: int = Field(gt=0, description="Number of hours in the simulation.", examples=[24]) + device_id: str = Field( + json_schema_extra={"description": "ID of device", "examples": ["device1"]} + ) + hours: int = Field( + gt=0, + json_schema_extra={"description": "Number of hours in the simulation.", "examples": [24]}, + ) class ElectricVehicleResult(DeviceOptimizeResult): """Result class containing information related to the electric vehicle's charging and discharging behavior.""" - device_id: str = Field(description="ID of electric vehicle", examples=["ev1"]) + device_id: str = Field( + json_schema_extra={"description": "ID of electric vehicle", "examples": ["ev1"]} + ) charge_array: list[float] = Field( - description="Hourly charging status (0 for no charging, 1 for charging)." + json_schema_extra={ + "description": "Hourly charging status (0 for no charging, 1 for charging)." + } ) discharge_array: list[int] = Field( - description="Hourly discharging status (0 for no discharging, 1 for discharging)." + json_schema_extra={ + "description": "Hourly discharging status (0 for no discharging, 1 for discharging)." + } + ) + discharging_efficiency: float = Field( + json_schema_extra={"description": "The discharge efficiency as a float.."} + ) + capacity_wh: int = Field( + json_schema_extra={"description": "Capacity of the EV’s battery in watt-hours."} + ) + charging_efficiency: float = Field( + json_schema_extra={"description": "Charging efficiency as a float.."} + ) + max_charge_power_w: int = Field( + json_schema_extra={"description": "Maximum charging power in watts."} ) - discharging_efficiency: float = Field(description="The discharge efficiency as a float..") - capacity_wh: int = Field(description="Capacity of the EV’s battery in watt-hours.") - charging_efficiency: float = Field(description="Charging efficiency as a float..") - max_charge_power_w: int = Field(description="Maximum charging power in watts.") soc_wh: float = Field( - description="State of charge of the battery in watt-hours at the start of the simulation." + json_schema_extra={ + "description": "State of charge of the battery in watt-hours at the start of the simulation." + } ) initial_soc_percentage: int = Field( - description="State of charge at the start of the simulation in percentage." + json_schema_extra={ + "description": "State of charge at the start of the simulation in percentage." + } ) @field_validator("discharge_array", "charge_array", mode="before") @@ -61,37 +84,49 @@ class ElectricVehicleResult(DeviceOptimizeResult): class GeneticSimulationResult(GeneticParametersBaseModel): """This object contains the results of the simulation and provides insights into various parameters over the entire forecast period.""" - Last_Wh_pro_Stunde: list[float] = Field(description="TBD") + Last_Wh_pro_Stunde: list[float] = Field(json_schema_extra={"description": "TBD"}) EAuto_SoC_pro_Stunde: list[float] = Field( - description="The state of charge of the EV for each hour." + json_schema_extra={"description": "The state of charge of the EV for each hour."} ) Einnahmen_Euro_pro_Stunde: list[float] = Field( - description="The revenue from grid feed-in or other sources in euros per hour." + json_schema_extra={ + "description": "The revenue from grid feed-in or other sources in euros per hour." + } ) Gesamt_Verluste: float = Field( - description="The total losses in watt-hours over the entire period." + json_schema_extra={"description": "The total losses in watt-hours over the entire period."} ) Gesamtbilanz_Euro: float = Field( - description="The total balance of revenues minus costs in euros." + json_schema_extra={"description": "The total balance of revenues minus costs in euros."} ) - Gesamteinnahmen_Euro: float = Field(description="The total revenues in euros.") - Gesamtkosten_Euro: float = Field(description="The total costs in euros.") + Gesamteinnahmen_Euro: float = Field( + json_schema_extra={"description": "The total revenues in euros."} + ) + Gesamtkosten_Euro: float = Field(json_schema_extra={"description": "The total costs in euros."}) Home_appliance_wh_per_hour: list[Optional[float]] = Field( - description="The energy consumption of a household appliance in watt-hours per hour." + json_schema_extra={ + "description": "The energy consumption of a household appliance in watt-hours per hour." + } + ) + Kosten_Euro_pro_Stunde: list[float] = Field( + json_schema_extra={"description": "The costs in euros per hour."} ) - Kosten_Euro_pro_Stunde: list[float] = Field(description="The costs in euros per hour.") Netzbezug_Wh_pro_Stunde: list[float] = Field( - description="The grid energy drawn in watt-hours per hour." + json_schema_extra={"description": "The grid energy drawn in watt-hours per hour."} ) Netzeinspeisung_Wh_pro_Stunde: list[float] = Field( - description="The energy fed into the grid in watt-hours per hour." + json_schema_extra={"description": "The energy fed into the grid in watt-hours per hour."} + ) + Verluste_Pro_Stunde: list[float] = Field( + json_schema_extra={"description": "The losses in watt-hours per hour."} ) - Verluste_Pro_Stunde: list[float] = Field(description="The losses in watt-hours per hour.") akku_soc_pro_stunde: list[float] = Field( - description="The state of charge of the battery (not the EV) in percentage per hour." + json_schema_extra={ + "description": "The state of charge of the battery (not the EV) in percentage per hour." + } ) Electricity_price: list[float] = Field( - description="Used Electricity Price, including predictions" + json_schema_extra={"description": "Used Electricity Price, including predictions"} ) @field_validator( @@ -115,24 +150,34 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): """**Note**: The first value of "Last_Wh_per_hour", "Netzeinspeisung_Wh_per_hour", and "Netzbezug_Wh_per_hour", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged.""" ac_charge: list[float] = Field( - description="Array with AC charging values as relative power (0.0-1.0), other values set to 0." + json_schema_extra={ + "description": "Array with AC charging values as relative power (0.0-1.0), other values set to 0." + } ) dc_charge: list[float] = Field( - description="Array with DC charging values as relative power (0-1), other values set to 0." + json_schema_extra={ + "description": "Array with DC charging values as relative power (0-1), other values set to 0." + } ) discharge_allowed: list[int] = Field( - description="Array with discharge values (1 for discharge, 0 otherwise)." + json_schema_extra={ + "description": "Array with discharge values (1 for discharge, 0 otherwise)." + } ) - eautocharge_hours_float: Optional[list[float]] = Field(description="TBD") + eautocharge_hours_float: Optional[list[float]] = Field(json_schema_extra={"description": "TBD"}) result: GeneticSimulationResult eauto_obj: Optional[ElectricVehicleResult] start_solution: Optional[list[float]] = Field( default=None, - description="An array of binary values (0 or 1) representing a possible starting solution for the simulation.", + json_schema_extra={ + "description": "An array of binary values (0 or 1) representing a possible starting solution for the simulation." + }, ) washingstart: Optional[int] = Field( default=None, - description="Can be `null` or contain an object representing the start of washing (if applicable).", + json_schema_extra={ + "description": "Can be `null` or contain an object representing the start of washing (if applicable)." + }, ) @field_validator( diff --git a/src/akkudoktoreos/optimization/optimization.py b/src/akkudoktoreos/optimization/optimization.py index 636a452..8aabacb 100644 --- a/src/akkudoktoreos/optimization/optimization.py +++ b/src/akkudoktoreos/optimization/optimization.py @@ -16,30 +16,38 @@ class GeneticCommonSettings(SettingsBaseModel): individuals: Optional[int] = Field( default=300, ge=10, - description="Number of individuals (solutions) to generate for the (initial) generation [>= 10]. Defaults to 300.", - examples=[300], + json_schema_extra={ + "description": "Number of individuals (solutions) to generate for the (initial) generation [>= 10]. Defaults to 300.", + "examples": [300], + }, ) generations: Optional[int] = Field( default=400, ge=10, - description="Number of generations to evaluate the optimal solution [>= 10]. Defaults to 400.", - examples=[400], + json_schema_extra={ + "description": "Number of generations to evaluate the optimal solution [>= 10]. Defaults to 400.", + "examples": [400], + }, ) seed: Optional[int] = Field( default=None, ge=0, - description="Fixed seed for genetic algorithm. Defaults to 'None' which means random seed.", - examples=[None], + json_schema_extra={ + "description": "Fixed seed for genetic algorithm. Defaults to 'None' which means random seed.", + "examples": [None], + }, ) penalties: Optional[dict[str, Union[float, int, str]]] = Field( default=None, - description="A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value.", - examples=[ - {"ev_soc_miss": 10}, - ], + json_schema_extra={ + "description": "A dictionary of penalty function parameters consisting of a penalty function parameter name and the associated value.", + "examples": [ + {"ev_soc_miss": 10}, + ], + }, ) @@ -49,28 +57,33 @@ class OptimizationCommonSettings(SettingsBaseModel): horizon_hours: Optional[int] = Field( default=24, ge=0, - description="The general time window within which the energy optimization goal shall be achieved [h]. Defaults to 24 hours.", - examples=[24], + json_schema_extra={ + "description": "The general time window within which the energy optimization goal shall be achieved [h]. Defaults to 24 hours.", + "examples": [24], + }, ) interval: Optional[int] = Field( default=3600, ge=15 * 60, le=60 * 60, - description="The optimization interval [sec].", - examples=[60 * 60, 15 * 60], + json_schema_extra={ + "description": "The optimization interval [sec].", + "examples": [60 * 60, 15 * 60], + }, ) algorithm: Optional[str] = Field( default="GENETIC", - description="The optimization algorithm.", - examples=["GENETIC"], + json_schema_extra={"description": "The optimization algorithm.", "examples": ["GENETIC"]}, ) genetic: Optional[GeneticCommonSettings] = Field( default=None, - description="Genetic optimization algorithm configuration.", - examples=[{"individuals": 400, "seed": None, "penalties": {"ev_soc_miss": 10}}], + json_schema_extra={ + "description": "Genetic optimization algorithm configuration.", + "examples": [{"individuals": 400, "seed": None, "penalties": {"ev_soc_miss": 10}}], + }, ) @model_validator(mode="after") @@ -85,57 +98,71 @@ class OptimizationCommonSettings(SettingsBaseModel): class OptimizationSolution(PydanticBaseModel): """General Optimization Solution.""" - id: str = Field(..., description="Unique ID for the optimization solution.") + id: str = Field( + ..., json_schema_extra={"description": "Unique ID for the optimization solution."} + ) - generated_at: DateTime = Field(..., description="Timestamp when the solution was generated.") + generated_at: DateTime = Field( + ..., json_schema_extra={"description": "Timestamp when the solution was generated."} + ) comment: Optional[str] = Field( - default=None, description="Optional comment or annotation for the solution." + default=None, + json_schema_extra={"description": "Optional comment or annotation for the solution."}, ) valid_from: Optional[DateTime] = Field( - default=None, description="Start time of the optimization solution." + default=None, json_schema_extra={"description": "Start time of the optimization solution."} ) valid_until: Optional[DateTime] = Field( - default=None, - description="End time of the optimization solution.", + default=None, json_schema_extra={"description": "End time of the optimization solution."} ) total_losses_energy_wh: float = Field( - description="The total losses in watt-hours over the entire period." + json_schema_extra={"description": "The total losses in watt-hours over the entire period."} ) - total_revenues_amt: float = Field(description="The total revenues [money amount].") + total_revenues_amt: float = Field( + json_schema_extra={"description": "The total revenues [money amount]."} + ) - total_costs_amt: float = Field(description="The total costs [money amount].") + total_costs_amt: float = Field( + json_schema_extra={"description": "The total costs [money amount]."} + ) - fitness_score: set[float] = Field(description="The fitness score as a set of fitness values.") + fitness_score: set[float] = Field( + json_schema_extra={"description": "The fitness score as a set of fitness values."} + ) prediction: PydanticDateTimeDataFrame = Field( - description=( - "Datetime data frame with time series prediction data per optimization interval:" - "- pv_energy_wh: PV energy prediction (positive) in wh" - "- elec_price_amt_kwh: Electricity price prediction in money per kwh" - "- feed_in_tariff_amt_kwh: Feed in tariff prediction in money per kwh" - "- weather_temp_air_celcius: Temperature in °C" - "- loadforecast_energy_wh: Load mean energy prediction in wh" - "- loadakkudoktor_std_energy_wh: Load energy standard deviation prediction in wh" - "- loadakkudoktor_mean_energy_wh: Load mean energy prediction in wh" - ) + json_schema_extra={ + "description": ( + "Datetime data frame with time series prediction data per optimization interval:" + "- pv_energy_wh: PV energy prediction (positive) in wh" + "- elec_price_amt_kwh: Electricity price prediction in money per kwh" + "- feed_in_tariff_amt_kwh: Feed in tariff prediction in money per kwh" + "- weather_temp_air_celcius: Temperature in °C" + "- loadforecast_energy_wh: Load mean energy prediction in wh" + "- loadakkudoktor_std_energy_wh: Load energy standard deviation prediction in wh" + "- loadakkudoktor_mean_energy_wh: Load mean energy prediction in wh" + ) + } ) solution: PydanticDateTimeDataFrame = Field( - description=( - "Datetime data frame with time series solution data per optimization interval:" - "- load_energy_wh: Load of all energy consumers in wh" - "- grid_energy_wh: Grid energy feed in (negative) or consumption (positive) in wh" - "- costs_amt: Costs in money amount" - "- revenue_amt: Revenue in money amount" - "- losses_energy_wh: Energy losses in wh" - "- _operation_mode_id: Operation mode id of the device." - "- _operation_mode_factor: Operation mode factor of the device." - "- _soc_factor: State of charge of a battery/ electric vehicle device as factor of total capacity." - "- _energy_wh: Energy consumption (positive) of a device in wh." - ) + json_schema_extra={ + "description": ( + "Datetime data frame with time series solution data per optimization interval:" + "- load_energy_wh: Load of all energy consumers in wh" + "- grid_energy_wh: Grid energy feed in (negative) or consumption (positive) in wh" + "- costs_amt: Costs in money amount" + "- revenue_amt: Revenue in money amount" + "- losses_energy_wh: Energy losses in wh" + "- _operation_mode_id: Operation mode id of the device." + "- _operation_mode_factor: Operation mode factor of the device." + "- _soc_factor: State of charge of a battery/ electric vehicle device as factor of total capacity." + "- _energy_wh: Energy consumption (positive) of a device in wh." + ) + } ) diff --git a/src/akkudoktoreos/prediction/elecprice.py b/src/akkudoktoreos/prediction/elecprice.py index 9a0f1a0..7d8c20f 100644 --- a/src/akkudoktoreos/prediction/elecprice.py +++ b/src/akkudoktoreos/prediction/elecprice.py @@ -21,7 +21,8 @@ class ElecPriceCommonProviderSettings(SettingsBaseModel): """Electricity Price Prediction Provider Configuration.""" ElecPriceImport: Optional[ElecPriceImportCommonSettings] = Field( - default=None, description="ElecPriceImport settings", examples=[None] + default=None, + json_schema_extra={"description": "ElecPriceImport settings", "examples": [None]}, ) @@ -30,31 +31,39 @@ class ElecPriceCommonSettings(SettingsBaseModel): provider: Optional[str] = Field( default=None, - description="Electricity price provider id of provider to be used.", - examples=["ElecPriceAkkudoktor"], + json_schema_extra={ + "description": "Electricity price provider id of provider to be used.", + "examples": ["ElecPriceAkkudoktor"], + }, ) charges_kwh: Optional[float] = Field( default=None, ge=0, - description="Electricity price charges [€/kWh]. Will be added to variable market price.", - examples=[0.21], + json_schema_extra={ + "description": "Electricity price charges [€/kWh]. Will be added to variable market price.", + "examples": [0.21], + }, ) vat_rate: Optional[float] = Field( default=1.19, ge=0, - description="VAT rate factor applied to electricity price when charges are used.", - examples=[1.19], + json_schema_extra={ + "description": "VAT rate factor applied to electricity price when charges are used.", + "examples": [1.19], + }, ) provider_settings: ElecPriceCommonProviderSettings = Field( default_factory=ElecPriceCommonProviderSettings, - description="Provider settings", - examples=[ - # Example 1: Empty/default settings (all providers None) - { - "ElecPriceImport": None, - }, - ], + json_schema_extra={ + "description": "Provider settings", + "examples": [ + # Example 1: Empty/default settings (all providers None) + { + "ElecPriceImport": None, + }, + ], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/elecpriceabc.py b/src/akkudoktoreos/prediction/elecpriceabc.py index 318b146..5a72313 100644 --- a/src/akkudoktoreos/prediction/elecpriceabc.py +++ b/src/akkudoktoreos/prediction/elecpriceabc.py @@ -21,7 +21,7 @@ class ElecPriceDataRecord(PredictionRecord): """ elecprice_marketprice_wh: Optional[float] = Field( - None, description="Electricity market price per Wh (€/Wh)" + None, json_schema_extra={"description": "Electricity market price per Wh (€/Wh)"} ) # Computed fields @@ -59,7 +59,8 @@ class ElecPriceProvider(PredictionProvider): # overload records: List[ElecPriceDataRecord] = Field( - default_factory=list, description="List of ElecPriceDataRecord records" + default_factory=list, + json_schema_extra={"description": "List of ElecPriceDataRecord records"}, ) @classmethod diff --git a/src/akkudoktoreos/prediction/elecpriceimport.py b/src/akkudoktoreos/prediction/elecpriceimport.py index 31e416f..65d89c7 100644 --- a/src/akkudoktoreos/prediction/elecpriceimport.py +++ b/src/akkudoktoreos/prediction/elecpriceimport.py @@ -22,14 +22,18 @@ class ElecPriceImportCommonSettings(SettingsBaseModel): import_file_path: Optional[Union[str, Path]] = Field( default=None, - description="Path to the file to import elecprice data from.", - examples=[None, "/path/to/prices.json"], + json_schema_extra={ + "description": "Path to the file to import elecprice data from.", + "examples": [None, "/path/to/prices.json"], + }, ) import_json: Optional[str] = Field( default=None, - description="JSON string, dictionary of electricity price forecast value lists.", - examples=['{"elecprice_marketprice_wh": [0.0003384, 0.0003318, 0.0003284]}'], + json_schema_extra={ + "description": "JSON string, dictionary of electricity price forecast value lists.", + "examples": ['{"elecprice_marketprice_wh": [0.0003384, 0.0003318, 0.0003284]}'], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/feedintariff.py b/src/akkudoktoreos/prediction/feedintariff.py index 821f7b8..3bf8a37 100644 --- a/src/akkudoktoreos/prediction/feedintariff.py +++ b/src/akkudoktoreos/prediction/feedintariff.py @@ -22,10 +22,12 @@ class FeedInTariffCommonProviderSettings(SettingsBaseModel): """Feed In Tariff Prediction Provider Configuration.""" FeedInTariffFixed: Optional[FeedInTariffFixedCommonSettings] = Field( - default=None, description="FeedInTariffFixed settings", examples=[None] + default=None, + json_schema_extra={"description": "FeedInTariffFixed settings", "examples": [None]}, ) FeedInTariffImport: Optional[FeedInTariffImportCommonSettings] = Field( - default=None, description="FeedInTariffImport settings", examples=[None] + default=None, + json_schema_extra={"description": "FeedInTariffImport settings", "examples": [None]}, ) @@ -34,20 +36,24 @@ class FeedInTariffCommonSettings(SettingsBaseModel): provider: Optional[str] = Field( default=None, - description="Feed in tariff provider id of provider to be used.", - examples=["FeedInTariffFixed", "FeedInTarifImport"], + json_schema_extra={ + "description": "Feed in tariff provider id of provider to be used.", + "examples": ["FeedInTariffFixed", "FeedInTarifImport"], + }, ) provider_settings: FeedInTariffCommonProviderSettings = Field( default_factory=FeedInTariffCommonProviderSettings, - description="Provider settings", - examples=[ - # Example 1: Empty/default settings (all providers None) - { - "FeedInTariffFixed": None, - "FeedInTariffImport": None, - }, - ], + json_schema_extra={ + "description": "Provider settings", + "examples": [ + # Example 1: Empty/default settings (all providers None) + { + "FeedInTariffFixed": None, + "FeedInTariffImport": None, + }, + ], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/feedintariffabc.py b/src/akkudoktoreos/prediction/feedintariffabc.py index ec76a63..85ef487 100644 --- a/src/akkudoktoreos/prediction/feedintariffabc.py +++ b/src/akkudoktoreos/prediction/feedintariffabc.py @@ -20,7 +20,9 @@ class FeedInTariffDataRecord(PredictionRecord): """ - feed_in_tariff_wh: Optional[float] = Field(None, description="Feed in tariff per Wh (€/Wh)") + feed_in_tariff_wh: Optional[float] = Field( + None, json_schema_extra={"description": "Feed in tariff per Wh (€/Wh)"} + ) # Computed fields @computed_field # type: ignore[prop-decorator] @@ -46,7 +48,8 @@ class FeedInTariffProvider(PredictionProvider): # overload records: List[FeedInTariffDataRecord] = Field( - default_factory=list, description="List of FeedInTariffDataRecord records" + default_factory=list, + json_schema_extra={"description": "List of FeedInTariffDataRecord records"}, ) @classmethod diff --git a/src/akkudoktoreos/prediction/feedintarifffixed.py b/src/akkudoktoreos/prediction/feedintarifffixed.py index e80cef6..e7f401c 100644 --- a/src/akkudoktoreos/prediction/feedintarifffixed.py +++ b/src/akkudoktoreos/prediction/feedintarifffixed.py @@ -16,8 +16,10 @@ class FeedInTariffFixedCommonSettings(SettingsBaseModel): feed_in_tariff_kwh: Optional[float] = Field( default=None, ge=0, - description="Electricity price feed in tariff [€/kWH].", - examples=[0.078], + json_schema_extra={ + "description": "Electricity price feed in tariff [€/kWH].", + "examples": [0.078], + }, ) diff --git a/src/akkudoktoreos/prediction/feedintariffimport.py b/src/akkudoktoreos/prediction/feedintariffimport.py index 4c30cfd..3ad8c06 100644 --- a/src/akkudoktoreos/prediction/feedintariffimport.py +++ b/src/akkudoktoreos/prediction/feedintariffimport.py @@ -21,13 +21,17 @@ class FeedInTariffImportCommonSettings(SettingsBaseModel): import_file_path: Optional[Union[str, Path]] = Field( default=None, - description="Path to the file to import feed in tariff data from.", - examples=[None, "/path/to/feedintariff.json"], + json_schema_extra={ + "description": "Path to the file to import feed in tariff data from.", + "examples": [None, "/path/to/feedintariff.json"], + }, ) import_json: Optional[str] = Field( default=None, - description="JSON string, dictionary of feed in tariff forecast value lists.", - examples=['{"fead_in_tariff_wh": [0.000078, 0.000078, 0.000023]}'], + json_schema_extra={ + "description": "JSON string, dictionary of feed in tariff forecast value lists.", + "examples": ['{"fead_in_tariff_wh": [0.000078, 0.000078, 0.000023]}'], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/load.py b/src/akkudoktoreos/prediction/load.py index 5dc8532..1921025 100644 --- a/src/akkudoktoreos/prediction/load.py +++ b/src/akkudoktoreos/prediction/load.py @@ -25,13 +25,14 @@ class LoadCommonProviderSettings(SettingsBaseModel): """Load Prediction Provider Configuration.""" LoadAkkudoktor: Optional[LoadAkkudoktorCommonSettings] = Field( - default=None, description="LoadAkkudoktor settings", examples=[None] + default=None, + json_schema_extra={"description": "LoadAkkudoktor settings", "examples": [None]}, ) LoadVrm: Optional[LoadVrmCommonSettings] = Field( - default=None, description="LoadVrm settings", examples=[None] + default=None, json_schema_extra={"description": "LoadVrm settings", "examples": [None]} ) LoadImport: Optional[LoadImportCommonSettings] = Field( - default=None, description="LoadImport settings", examples=[None] + default=None, json_schema_extra={"description": "LoadImport settings", "examples": [None]} ) @@ -40,21 +41,25 @@ class LoadCommonSettings(SettingsBaseModel): provider: Optional[str] = Field( default=None, - description="Load provider id of provider to be used.", - examples=["LoadAkkudoktor"], + json_schema_extra={ + "description": "Load provider id of provider to be used.", + "examples": ["LoadAkkudoktor"], + }, ) provider_settings: LoadCommonProviderSettings = Field( default_factory=LoadCommonProviderSettings, - description="Provider settings", - examples=[ - # Example 1: Empty/default settings (all providers None) - { - "LoadAkkudoktor": None, - "LoadVrm": None, - "LoadImport": None, - }, - ], + json_schema_extra={ + "description": "Provider settings", + "examples": [ + # Example 1: Empty/default settings (all providers None) + { + "LoadAkkudoktor": None, + "LoadVrm": None, + "LoadImport": None, + }, + ], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/loadabc.py b/src/akkudoktoreos/prediction/loadabc.py index c7f5a87..bd0d818 100644 --- a/src/akkudoktoreos/prediction/loadabc.py +++ b/src/akkudoktoreos/prediction/loadabc.py @@ -16,7 +16,7 @@ class LoadDataRecord(PredictionRecord): """Represents a load data record containing various load attributes at a specific datetime.""" loadforecast_power_w: Optional[float] = Field( - default=None, description="Predicted load mean value (W)." + default=None, json_schema_extra={"description": "Predicted load mean value (W)."} ) @@ -42,7 +42,7 @@ class LoadProvider(PredictionProvider): # overload records: List[LoadDataRecord] = Field( - default_factory=list, description="List of LoadDataRecord records" + default_factory=list, json_schema_extra={"description": "List of LoadDataRecord records"} ) @classmethod diff --git a/src/akkudoktoreos/prediction/loadakkudoktor.py b/src/akkudoktoreos/prediction/loadakkudoktor.py index 640449c..b8aa11e 100644 --- a/src/akkudoktoreos/prediction/loadakkudoktor.py +++ b/src/akkudoktoreos/prediction/loadakkudoktor.py @@ -15,7 +15,8 @@ class LoadAkkudoktorCommonSettings(SettingsBaseModel): """Common settings for load data import from file.""" loadakkudoktor_year_energy_kwh: Optional[float] = Field( - default=None, description="Yearly energy consumption (kWh).", examples=[40421] + default=None, + json_schema_extra={"description": "Yearly energy consumption (kWh).", "examples": [40421]}, ) @@ -23,11 +24,11 @@ class LoadAkkudoktorDataRecord(LoadDataRecord): """Represents a load data record with extra fields for LoadAkkudoktor.""" loadakkudoktor_mean_power_w: Optional[float] = Field( - default=None, description="Predicted load mean value (W)." + default=None, json_schema_extra={"description": "Predicted load mean value (W)."} ) loadakkudoktor_std_power_w: Optional[float] = Field( - default=None, description="Predicted load standard deviation (W)." + default=None, json_schema_extra={"description": "Predicted load standard deviation (W)."} ) @@ -35,7 +36,8 @@ class LoadAkkudoktor(LoadProvider): """Fetch Load forecast data from Akkudoktor load profiles.""" records: list[LoadAkkudoktorDataRecord] = Field( - default_factory=list, description="List of LoadAkkudoktorDataRecord records" + default_factory=list, + json_schema_extra={"description": "List of LoadAkkudoktorDataRecord records"}, ) @classmethod diff --git a/src/akkudoktoreos/prediction/loadimport.py b/src/akkudoktoreos/prediction/loadimport.py index 38e0fb4..4f09738 100644 --- a/src/akkudoktoreos/prediction/loadimport.py +++ b/src/akkudoktoreos/prediction/loadimport.py @@ -22,13 +22,17 @@ class LoadImportCommonSettings(SettingsBaseModel): import_file_path: Optional[Union[str, Path]] = Field( default=None, - description="Path to the file to import load data from.", - examples=[None, "/path/to/yearly_load.json"], + json_schema_extra={ + "description": "Path to the file to import load data from.", + "examples": [None, "/path/to/yearly_load.json"], + }, ) import_json: Optional[str] = Field( default=None, - description="JSON string, dictionary of load forecast value lists.", - examples=['{"load0_mean": [676.71, 876.19, 527.13]}'], + json_schema_extra={ + "description": "JSON string, dictionary of load forecast value lists.", + "examples": ['{"load0_mean": [676.71, 876.19, 527.13]}'], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/loadvrm.py b/src/akkudoktoreos/prediction/loadvrm.py index bb200b5..0a10866 100644 --- a/src/akkudoktoreos/prediction/loadvrm.py +++ b/src/akkudoktoreos/prediction/loadvrm.py @@ -27,9 +27,15 @@ class LoadVrmCommonSettings(SettingsBaseModel): """Common settings for VRM API.""" load_vrm_token: str = Field( - default="your-token", description="Token for Connecting VRM API", examples=["your-token"] + default="your-token", + json_schema_extra={ + "description": "Token for Connecting VRM API", + "examples": ["your-token"], + }, + ) + load_vrm_idsite: int = Field( + default=12345, json_schema_extra={"description": "VRM-Installation-ID", "examples": [12345]} ) - load_vrm_idsite: int = Field(default=12345, description="VRM-Installation-ID", examples=[12345]) class LoadVrm(LoadProvider): diff --git a/src/akkudoktoreos/prediction/prediction.py b/src/akkudoktoreos/prediction/prediction.py index e55f5ba..b90d1ed 100644 --- a/src/akkudoktoreos/prediction/prediction.py +++ b/src/akkudoktoreos/prediction/prediction.py @@ -70,13 +70,17 @@ class PredictionCommonSettings(SettingsBaseModel): """ hours: Optional[int] = Field( - default=48, ge=0, description="Number of hours into the future for predictions" + default=48, + ge=0, + json_schema_extra={"description": "Number of hours into the future for predictions"}, ) historic_hours: Optional[int] = Field( default=48, ge=0, - description="Number of hours into the past for historical predictions data", + json_schema_extra={ + "description": "Number of hours into the past for historical predictions data" + }, ) @@ -107,7 +111,9 @@ class Prediction(PredictionContainer): WeatherClearOutside, WeatherImport, ] - ] = Field(default_factory=list, description="List of prediction providers") + ] = Field( + default_factory=list, json_schema_extra={"description": "List of prediction providers"} + ) # Initialize forecast providers, all are singletons. diff --git a/src/akkudoktoreos/prediction/predictionabc.py b/src/akkudoktoreos/prediction/predictionabc.py index 4aa2e12..51e23f9 100644 --- a/src/akkudoktoreos/prediction/predictionabc.py +++ b/src/akkudoktoreos/prediction/predictionabc.py @@ -72,8 +72,7 @@ class PredictionSequence(DataSequence): Usage: # Example of creating, adding, and using PredictionSequence class DerivedSequence(PredictionSquence): - records: List[DerivedPredictionRecord] = Field(default_factory=list, - description="List of prediction records") + records: List[DerivedPredictionRecord] = Field(default_factory=list, json_schema_extra={ "description": "List of prediction records" }) seq = DerivedSequence() seq.insert(DerivedPredictionRecord(date_time=datetime.now(), temperature=72)) @@ -89,7 +88,7 @@ class PredictionSequence(DataSequence): # To be overloaded by derived classes. records: List[PredictionRecord] = Field( - default_factory=list, description="List of prediction records" + default_factory=list, json_schema_extra={"description": "List of prediction records"} ) @@ -249,5 +248,5 @@ class PredictionContainer(PredictionStartEndKeepMixin, DataContainer): # To be overloaded by derived classes. providers: List[PredictionProvider] = Field( - default_factory=list, description="List of prediction providers" + default_factory=list, json_schema_extra={"description": "List of prediction providers"} ) diff --git a/src/akkudoktoreos/prediction/pvforecast.py b/src/akkudoktoreos/prediction/pvforecast.py index f26b468..43c8a36 100644 --- a/src/akkudoktoreos/prediction/pvforecast.py +++ b/src/akkudoktoreos/prediction/pvforecast.py @@ -23,77 +23,118 @@ pvforecast_providers = [ class PVForecastPlaneSetting(SettingsBaseModel): """PV Forecast Plane Configuration.""" - # latitude: Optional[float] = Field(default=None, description="Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)") + # latitude: Optional[float] = Field(default=None, json_schema_extra={ "description": "Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)" }) surface_tilt: Optional[float] = Field( default=30.0, ge=0.0, le=90.0, - description="Tilt angle from horizontal plane. Ignored for two-axis tracking.", - examples=[10.0, 20.0], + json_schema_extra={ + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", + "examples": [10.0, 20.0], + }, ) surface_azimuth: Optional[float] = Field( default=180.0, ge=0.0, le=360.0, - description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - examples=[180.0, 90.0], + json_schema_extra={ + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", + "examples": [180.0, 90.0], + }, ) userhorizon: Optional[List[float]] = Field( default=None, - description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.", - examples=[[10.0, 20.0, 30.0], [5.0, 15.0, 25.0]], + json_schema_extra={ + "description": "Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.", + "examples": [[10.0, 20.0, 30.0], [5.0, 15.0, 25.0]], + }, ) peakpower: Optional[float] = Field( - default=None, description="Nominal power of PV system in kW.", examples=[5.0, 3.5] + default=None, + json_schema_extra={ + "description": "Nominal power of PV system in kW.", + "examples": [5.0, 3.5], + }, ) pvtechchoice: Optional[str] = Field( - default="crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'." + default="crystSi", + json_schema_extra={ + "description": "PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'." + }, ) mountingplace: Optional[str] = Field( default="free", - description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.", + json_schema_extra={ + "description": "Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated." + }, + ) + loss: Optional[float] = Field( + default=14.0, json_schema_extra={"description": "Sum of PV system losses in percent"} ) - loss: Optional[float] = Field(default=14.0, description="Sum of PV system losses in percent") trackingtype: Optional[int] = Field( default=None, ge=0, le=5, - description="Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - examples=[0, 1, 2, 3, 4, 5], + json_schema_extra={ + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", + "examples": [0, 1, 2, 3, 4, 5], + }, ) optimal_surface_tilt: Optional[bool] = Field( default=False, - description="Calculate the optimum tilt angle. Ignored for two-axis tracking.", - examples=[False], + json_schema_extra={ + "description": "Calculate the optimum tilt angle. Ignored for two-axis tracking.", + "examples": [False], + }, ) optimalangles: Optional[bool] = Field( default=False, - description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.", - examples=[False], + json_schema_extra={ + "description": "Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.", + "examples": [False], + }, ) albedo: Optional[float] = Field( default=None, - description="Proportion of the light hitting the ground that it reflects back.", - examples=[None], + json_schema_extra={ + "description": "Proportion of the light hitting the ground that it reflects back.", + "examples": [None], + }, ) module_model: Optional[str] = Field( - default=None, description="Model of the PV modules of this plane.", examples=[None] + default=None, + json_schema_extra={ + "description": "Model of the PV modules of this plane.", + "examples": [None], + }, ) inverter_model: Optional[str] = Field( - default=None, description="Model of the inverter of this plane.", examples=[None] + default=None, + json_schema_extra={ + "description": "Model of the inverter of this plane.", + "examples": [None], + }, ) inverter_paco: Optional[int] = Field( - default=None, description="AC power rating of the inverter [W].", examples=[6000, 4000] + default=None, + json_schema_extra={ + "description": "AC power rating of the inverter [W].", + "examples": [6000, 4000], + }, ) modules_per_string: Optional[int] = Field( default=None, - description="Number of the PV modules of the strings of this plane.", - examples=[20], + json_schema_extra={ + "description": "Number of the PV modules of the strings of this plane.", + "examples": [20], + }, ) strings_per_inverter: Optional[int] = Field( default=None, - description="Number of the strings of the inverter of this plane.", - examples=[2], + json_schema_extra={ + "description": "Number of the strings of the inverter of this plane.", + "examples": [2], + }, ) @model_validator(mode="after") @@ -124,10 +165,12 @@ class PVForecastCommonProviderSettings(SettingsBaseModel): """PV Forecast Provider Configuration.""" PVForecastImport: Optional[PVForecastImportCommonSettings] = Field( - default=None, description="PVForecastImport settings", examples=[None] + default=None, + json_schema_extra={"description": "PVForecastImport settings", "examples": [None]}, ) PVForecastVrm: Optional[PVForecastVrmCommonSettings] = Field( - default=None, description="PVForecastVrm settings", examples=[None] + default=None, + json_schema_extra={"description": "PVForecastVrm settings", "examples": [None]}, ) @@ -141,72 +184,80 @@ class PVForecastCommonSettings(SettingsBaseModel): provider: Optional[str] = Field( default=None, - description="PVForecast provider id of provider to be used.", - examples=["PVForecastAkkudoktor"], + json_schema_extra={ + "description": "PVForecast provider id of provider to be used.", + "examples": ["PVForecastAkkudoktor"], + }, ) provider_settings: PVForecastCommonProviderSettings = Field( default_factory=PVForecastCommonProviderSettings, - description="Provider settings", - examples=[ - # Example 1: Empty/default settings (all providers None) - { - "PVForecastImport": None, - "PVForecastVrm": None, - }, - ], + json_schema_extra={ + "description": "Provider settings", + "examples": [ + # Example 1: Empty/default settings (all providers None) + { + "PVForecastImport": None, + "PVForecastVrm": None, + }, + ], + }, ) planes: Optional[list[PVForecastPlaneSetting]] = Field( default=None, - description="Plane configuration.", - examples=[ - [ - { - "surface_tilt": 10.0, - "surface_azimuth": 180.0, - "userhorizon": [10.0, 20.0, 30.0], - "peakpower": 5.0, - "pvtechchoice": "crystSi", - "mountingplace": "free", - "loss": 14.0, - "trackingtype": 0, - "optimal_surface_tilt": False, - "optimalangles": False, - "albedo": None, - "module_model": None, - "inverter_model": None, - "inverter_paco": 6000, - "modules_per_string": 20, - "strings_per_inverter": 2, - }, - { - "surface_tilt": 20.0, - "surface_azimuth": 90.0, - "userhorizon": [5.0, 15.0, 25.0], - "peakpower": 3.5, - "pvtechchoice": "crystSi", - "mountingplace": "free", - "loss": 14.0, - "trackingtype": 1, - "optimal_surface_tilt": False, - "optimalangles": False, - "albedo": None, - "module_model": None, - "inverter_model": None, - "inverter_paco": 4000, - "modules_per_string": 20, - "strings_per_inverter": 2, - }, - ] - ], + json_schema_extra={ + "description": "Plane configuration.", + "examples": [ + [ + { + "surface_tilt": 10.0, + "surface_azimuth": 180.0, + "userhorizon": [10.0, 20.0, 30.0], + "peakpower": 5.0, + "pvtechchoice": "crystSi", + "mountingplace": "free", + "loss": 14.0, + "trackingtype": 0, + "optimal_surface_tilt": False, + "optimalangles": False, + "albedo": None, + "module_model": None, + "inverter_model": None, + "inverter_paco": 6000, + "modules_per_string": 20, + "strings_per_inverter": 2, + }, + { + "surface_tilt": 20.0, + "surface_azimuth": 90.0, + "userhorizon": [5.0, 15.0, 25.0], + "peakpower": 3.5, + "pvtechchoice": "crystSi", + "mountingplace": "free", + "loss": 14.0, + "trackingtype": 1, + "optimal_surface_tilt": False, + "optimalangles": False, + "albedo": None, + "module_model": None, + "inverter_model": None, + "inverter_paco": 4000, + "modules_per_string": 20, + "strings_per_inverter": 2, + }, + ] + ], + }, ) max_planes: Optional[int] = Field( default=0, ge=0, - description="Maximum number of planes that can be set", - examples=[1, 2], + json_schema_extra={ + "description": "Maximum number of planes that can be set", + "examples": [1, 2], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/pvforecastabc.py b/src/akkudoktoreos/prediction/pvforecastabc.py index acbe5d3..84c0b50 100644 --- a/src/akkudoktoreos/prediction/pvforecastabc.py +++ b/src/akkudoktoreos/prediction/pvforecastabc.py @@ -16,8 +16,12 @@ from akkudoktoreos.prediction.predictionabc import PredictionProvider, Predictio class PVForecastDataRecord(PredictionRecord): """Represents a pvforecast data record containing various pvforecast attributes at a specific datetime.""" - pvforecast_dc_power: Optional[float] = Field(default=None, description="Total DC power (W).") - pvforecast_ac_power: Optional[float] = Field(default=None, description="Total AC power (W).") + pvforecast_dc_power: Optional[float] = Field( + default=None, json_schema_extra={"description": "Total DC power (W)."} + ) + pvforecast_ac_power: Optional[float] = Field( + default=None, json_schema_extra={"description": "Total AC power (W)."} + ) class PVForecastProvider(PredictionProvider): @@ -42,7 +46,8 @@ class PVForecastProvider(PredictionProvider): # overload records: List[PVForecastDataRecord] = Field( - default_factory=list, description="List of PVForecastDataRecord records" + default_factory=list, + json_schema_extra={"description": "List of PVForecastDataRecord records"}, ) @classmethod diff --git a/src/akkudoktoreos/prediction/pvforecastakkudoktor.py b/src/akkudoktoreos/prediction/pvforecastakkudoktor.py index 859fefd..c8b09f2 100644 --- a/src/akkudoktoreos/prediction/pvforecastakkudoktor.py +++ b/src/akkudoktoreos/prediction/pvforecastakkudoktor.py @@ -157,13 +157,13 @@ class PVForecastAkkudoktorDataRecord(PVForecastDataRecord): """Represents a Akkudoktor specific pvforecast data record containing various pvforecast attributes at a specific datetime.""" pvforecastakkudoktor_ac_power_measured: Optional[float] = Field( - default=None, description="Total AC power measured (W)" + default=None, json_schema_extra={"description": "Total AC power measured (W)"} ) pvforecastakkudoktor_wind_speed_10m: Optional[float] = Field( - default=None, description="Wind Speed 10m (kmph)" + default=None, json_schema_extra={"description": "Wind Speed 10m (kmph)"} ) pvforecastakkudoktor_temp_air: Optional[float] = Field( - default=None, description="Temperature (°C)" + default=None, json_schema_extra={"description": "Temperature (°C)"} ) # Computed fields @@ -209,7 +209,8 @@ class PVForecastAkkudoktor(PVForecastProvider): # overload records: List[PVForecastAkkudoktorDataRecord] = Field( - default_factory=list, description="List of PVForecastAkkudoktorDataRecord records" + default_factory=list, + json_schema_extra={"description": "List of PVForecastAkkudoktorDataRecord records"}, ) @classmethod diff --git a/src/akkudoktoreos/prediction/pvforecastimport.py b/src/akkudoktoreos/prediction/pvforecastimport.py index bccfc2c..78df2d9 100644 --- a/src/akkudoktoreos/prediction/pvforecastimport.py +++ b/src/akkudoktoreos/prediction/pvforecastimport.py @@ -22,14 +22,18 @@ class PVForecastImportCommonSettings(SettingsBaseModel): import_file_path: Optional[Union[str, Path]] = Field( default=None, - description="Path to the file to import PV forecast data from.", - examples=[None, "/path/to/pvforecast.json"], + json_schema_extra={ + "description": "Path to the file to import PV forecast data from.", + "examples": [None, "/path/to/pvforecast.json"], + }, ) import_json: Optional[str] = Field( default=None, - description="JSON string, dictionary of PV forecast value lists.", - examples=['{"pvforecast_ac_power": [0, 8.05, 352.91]}'], + json_schema_extra={ + "description": "JSON string, dictionary of PV forecast value lists.", + "examples": ['{"pvforecast_ac_power": [0, 8.05, 352.91]}'], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/pvforecastvrm.py b/src/akkudoktoreos/prediction/pvforecastvrm.py index d5f4832..5761bd6 100644 --- a/src/akkudoktoreos/prediction/pvforecastvrm.py +++ b/src/akkudoktoreos/prediction/pvforecastvrm.py @@ -27,10 +27,14 @@ class PVForecastVrmCommonSettings(SettingsBaseModel): """Common settings for VRM API.""" pvforecast_vrm_token: str = Field( - default="your-token", description="Token for Connecting VRM API", examples=["your-token"] + default="your-token", + json_schema_extra={ + "description": "Token for Connecting VRM API", + "examples": ["your-token"], + }, ) pvforecast_vrm_idsite: int = Field( - default=12345, description="VRM-Installation-ID", examples=[12345] + default=12345, json_schema_extra={"description": "VRM-Installation-ID", "examples": [12345]} ) diff --git a/src/akkudoktoreos/prediction/weather.py b/src/akkudoktoreos/prediction/weather.py index fc8244a..25d982f 100644 --- a/src/akkudoktoreos/prediction/weather.py +++ b/src/akkudoktoreos/prediction/weather.py @@ -23,7 +23,8 @@ class WeatherCommonProviderSettings(SettingsBaseModel): """Weather Forecast Provider Configuration.""" WeatherImport: Optional[WeatherImportCommonSettings] = Field( - default=None, description="WeatherImport settings", examples=[None] + default=None, + json_schema_extra={"description": "WeatherImport settings", "examples": [None]}, ) @@ -32,19 +33,23 @@ class WeatherCommonSettings(SettingsBaseModel): provider: Optional[str] = Field( default=None, - description="Weather provider id of provider to be used.", - examples=["WeatherImport"], + json_schema_extra={ + "description": "Weather provider id of provider to be used.", + "examples": ["WeatherImport"], + }, ) provider_settings: WeatherCommonProviderSettings = Field( default_factory=WeatherCommonProviderSettings, - description="Provider settings", - examples=[ - # Example 1: Empty/default settings (all providers None) - { - "WeatherImport": None, - }, - ], + json_schema_extra={ + "description": "Provider settings", + "examples": [ + # Example 1: Empty/default settings (all providers None) + { + "WeatherImport": None, + }, + ], + }, ) # Validators diff --git a/src/akkudoktoreos/prediction/weatherabc.py b/src/akkudoktoreos/prediction/weatherabc.py index c98c046..ae80342 100644 --- a/src/akkudoktoreos/prediction/weatherabc.py +++ b/src/akkudoktoreos/prediction/weatherabc.py @@ -47,48 +47,68 @@ class WeatherDataRecord(PredictionRecord): """ weather_total_clouds: Optional[float] = Field( - default=None, description="Total Clouds (% Sky Obscured)" + default=None, json_schema_extra={"description": "Total Clouds (% Sky Obscured)"} ) weather_low_clouds: Optional[float] = Field( - default=None, description="Low Clouds (% Sky Obscured)" + default=None, json_schema_extra={"description": "Low Clouds (% Sky Obscured)"} ) weather_medium_clouds: Optional[float] = Field( - default=None, description="Medium Clouds (% Sky Obscured)" + default=None, json_schema_extra={"description": "Medium Clouds (% Sky Obscured)"} ) weather_high_clouds: Optional[float] = Field( - default=None, description="High Clouds (% Sky Obscured)" + default=None, json_schema_extra={"description": "High Clouds (% Sky Obscured)"} + ) + weather_visibility: Optional[float] = Field( + default=None, json_schema_extra={"description": "Visibility (m)"} + ) + weather_fog: Optional[float] = Field(default=None, json_schema_extra={"description": "Fog (%)"}) + weather_precip_type: Optional[str] = Field( + default=None, json_schema_extra={"description": "Precipitation Type"} ) - weather_visibility: Optional[float] = Field(default=None, description="Visibility (m)") - weather_fog: Optional[float] = Field(default=None, description="Fog (%)") - weather_precip_type: Optional[str] = Field(default=None, description="Precipitation Type") weather_precip_prob: Optional[float] = Field( - default=None, description="Precipitation Probability (%)" + default=None, json_schema_extra={"description": "Precipitation Probability (%)"} ) weather_precip_amt: Optional[float] = Field( - default=None, description="Precipitation Amount (mm)" + default=None, json_schema_extra={"description": "Precipitation Amount (mm)"} ) weather_preciptable_water: Optional[float] = Field( - default=None, description="Precipitable Water (cm)" + default=None, json_schema_extra={"description": "Precipitable Water (cm)"} + ) + weather_wind_speed: Optional[float] = Field( + default=None, json_schema_extra={"description": "Wind Speed (kmph)"} + ) + weather_wind_direction: Optional[float] = Field( + default=None, json_schema_extra={"description": "Wind Direction (°)"} + ) + weather_frost_chance: Optional[str] = Field( + default=None, json_schema_extra={"description": "Chance of Frost"} + ) + weather_temp_air: Optional[float] = Field( + default=None, json_schema_extra={"description": "Temperature (°C)"} + ) + weather_feels_like: Optional[float] = Field( + default=None, json_schema_extra={"description": "Feels Like (°C)"} + ) + weather_dew_point: Optional[float] = Field( + default=None, json_schema_extra={"description": "Dew Point (°C)"} ) - weather_wind_speed: Optional[float] = Field(default=None, description="Wind Speed (kmph)") - weather_wind_direction: Optional[float] = Field(default=None, description="Wind Direction (°)") - weather_frost_chance: Optional[str] = Field(default=None, description="Chance of Frost") - weather_temp_air: Optional[float] = Field(default=None, description="Temperature (°C)") - weather_feels_like: Optional[float] = Field(default=None, description="Feels Like (°C)") - weather_dew_point: Optional[float] = Field(default=None, description="Dew Point (°C)") weather_relative_humidity: Optional[float] = Field( - default=None, description="Relative Humidity (%)" + default=None, json_schema_extra={"description": "Relative Humidity (%)"} + ) + weather_pressure: Optional[float] = Field( + default=None, json_schema_extra={"description": "Pressure (mb)"} + ) + weather_ozone: Optional[float] = Field( + default=None, json_schema_extra={"description": "Ozone (du)"} ) - weather_pressure: Optional[float] = Field(default=None, description="Pressure (mb)") - weather_ozone: Optional[float] = Field(default=None, description="Ozone (du)") weather_ghi: Optional[float] = Field( - default=None, description="Global Horizontal Irradiance (W/m2)" + default=None, json_schema_extra={"description": "Global Horizontal Irradiance (W/m2)"} ) weather_dni: Optional[float] = Field( - default=None, description="Direct Normal Irradiance (W/m2)" + default=None, json_schema_extra={"description": "Direct Normal Irradiance (W/m2)"} ) weather_dhi: Optional[float] = Field( - default=None, description="Diffuse Horizontal Irradiance (W/m2)" + default=None, json_schema_extra={"description": "Diffuse Horizontal Irradiance (W/m2)"} ) @@ -114,7 +134,7 @@ class WeatherProvider(PredictionProvider): # overload records: List[WeatherDataRecord] = Field( - default_factory=list, description="List of WeatherDataRecord records" + default_factory=list, json_schema_extra={"description": "List of WeatherDataRecord records"} ) @classmethod diff --git a/src/akkudoktoreos/prediction/weatherimport.py b/src/akkudoktoreos/prediction/weatherimport.py index 098667d..4842fe1 100644 --- a/src/akkudoktoreos/prediction/weatherimport.py +++ b/src/akkudoktoreos/prediction/weatherimport.py @@ -22,14 +22,18 @@ class WeatherImportCommonSettings(SettingsBaseModel): import_file_path: Optional[Union[str, Path]] = Field( default=None, - description="Path to the file to import weather data from.", - examples=[None, "/path/to/weather_data.json"], + json_schema_extra={ + "description": "Path to the file to import weather data from.", + "examples": [None, "/path/to/weather_data.json"], + }, ) import_json: Optional[str] = Field( default=None, - description="JSON string, dictionary of weather forecast value lists.", - examples=['{"weather_temp_air": [18.3, 17.8, 16.9]}'], + json_schema_extra={ + "description": "JSON string, dictionary of weather forecast value lists.", + "examples": ['{"weather_temp_air": [18.3, 17.8, 16.9]}'], + }, ) # Validators diff --git a/src/akkudoktoreos/server/dash/configuration.py b/src/akkudoktoreos/server/dash/configuration.py index a7c37dc..fa7210a 100644 --- a/src/akkudoktoreos/server/dash/configuration.py +++ b/src/akkudoktoreos/server/dash/configuration.py @@ -77,6 +77,65 @@ def get_nested_value( return default +def get_field_extra_dict( + subfield_info: Union[FieldInfo, ComputedFieldInfo], +) -> Dict[str, Any]: + """Extract json_schema_extra. + + Extract regardless of whether it is defined directly + on the field (Pydantic v2) or inherited from v1 compatibility wrappers. + Always returns a dictionary. + """ + # Pydantic v2 location + extra = getattr(subfield_info, "json_schema_extra", None) + if isinstance(extra, dict): + return extra + + # Pydantic v1 compatibility fallbacks + fi = getattr(subfield_info, "field_info", None) + if fi is not None: + extra = getattr(fi, "json_schema_extra", None) + if isinstance(extra, dict): + return extra + + return {} + + +def get_description( + subfield_info: Union[FieldInfo, ComputedFieldInfo], + extra: Dict[str, Any], +) -> str: + """Fetch description. + + Priority: + 1) json_schema_extra["description"] + 2) field_info.description + 3) empty string + """ + if "description" in extra: + return str(extra["description"]) + + desc = getattr(subfield_info, "description", None) + return str(desc) if desc is not None else "" + + +def get_deprecated( + subfield_info: Union[FieldInfo, ComputedFieldInfo], + extra: Dict[str, Any], +) -> Optional[Any]: + """Fetch deprecated. + + Priority: + 1) json_schema_extra["deprecated"] + 2) field_info.deprecated + 3) None + """ + if "deprecated" in extra: + return extra["deprecated"] + + return getattr(subfield_info, "deprecated", None) + + def get_default_value(field_info: Union[FieldInfo, ComputedFieldInfo], regular_field: bool) -> Any: """Retrieve the default value of a field. @@ -163,6 +222,7 @@ def configuration( ): if found_basic: continue + extra = get_field_extra_dict(subfield_info) config: dict[str, Optional[Any]] = {} config["name"] = ".".join(values_prefix + parent_types) @@ -170,12 +230,8 @@ def configuration( get_nested_value(values, values_prefix + parent_types, "") ) config["default"] = json.dumps(get_default_value(subfield_info, regular_field)) - config["description"] = ( - subfield_info.description if subfield_info.description else "" - ) - config["deprecated"] = ( - subfield_info.deprecated if subfield_info.deprecated else None - ) + config["description"] = get_description(subfield_info, extra) + config["deprecated"] = get_deprecated(subfield_info, extra) if isinstance(subfield_info, ComputedFieldInfo): config["read-only"] = "ro" type_description = str(subfield_info.return_type) diff --git a/src/akkudoktoreos/server/server.py b/src/akkudoktoreos/server/server.py index becd245..a782b03 100644 --- a/src/akkudoktoreos/server/server.py +++ b/src/akkudoktoreos/server/server.py @@ -153,31 +153,42 @@ class ServerCommonSettings(SettingsBaseModel): host: Optional[str] = Field( default=get_default_host(), - description="EOS server IP address. Defaults to 127.0.0.1.", - examples=["127.0.0.1", "localhost"], + json_schema_extra={ + "description": "EOS server IP address. Defaults to 127.0.0.1.", + "examples": ["127.0.0.1", "localhost"], + }, ) port: Optional[int] = Field( default=8503, - description="EOS server IP port number. Defaults to 8503.", - examples=[ - 8503, - ], + json_schema_extra={ + "description": "EOS server IP port number. Defaults to 8503.", + "examples": [ + 8503, + ], + }, + ) + verbose: Optional[bool] = Field( + default=False, json_schema_extra={"description": "Enable debug output"} ) - verbose: Optional[bool] = Field(default=False, description="Enable debug output") startup_eosdash: Optional[bool] = Field( - default=True, description="EOS server to start EOSdash server. Defaults to True." + default=True, + json_schema_extra={"description": "EOS server to start EOSdash server. Defaults to True."}, ) eosdash_host: Optional[str] = Field( default=None, - description="EOSdash server IP address. Defaults to EOS server IP address.", - examples=["127.0.0.1", "localhost"], + json_schema_extra={ + "description": "EOSdash server IP address. Defaults to EOS server IP address.", + "examples": ["127.0.0.1", "localhost"], + }, ) eosdash_port: Optional[int] = Field( default=None, - description="EOSdash server IP port number. Defaults to EOS server IP port number + 1.", - examples=[ - 8504, - ], + json_schema_extra={ + "description": "EOSdash server IP port number. Defaults to EOS server IP port number + 1.", + "examples": [ + 8504, + ], + }, ) @field_validator("host", "eosdash_host", mode="before") diff --git a/src/akkudoktoreos/utils/datetimeutil.py b/src/akkudoktoreos/utils/datetimeutil.py index fc219b7..9d9e18d 100644 --- a/src/akkudoktoreos/utils/datetimeutil.py +++ b/src/akkudoktoreos/utils/datetimeutil.py @@ -835,31 +835,42 @@ class TimeWindow(BaseModel): Supports day names in multiple languages via locale-aware parsing. """ - start_time: Time = Field(..., description="Start time of the time window (time of day).") + start_time: Time = Field( + ..., json_schema_extra={"description": "Start time of the time window (time of day)."} + ) duration: Duration = Field( - ..., description="Duration of the time window starting from `start_time`." + ..., + json_schema_extra={ + "description": "Duration of the time window starting from `start_time`." + }, ) day_of_week: Optional[Union[int, str]] = Field( default=None, - description=( - "Optional day of the week restriction. " - "Can be specified as integer (0=Monday to 6=Sunday) or localized weekday name. " - "If None, applies every day unless `date` is set." - ), + json_schema_extra={ + "description": ( + "Optional day of the week restriction. " + "Can be specified as integer (0=Monday to 6=Sunday) or localized weekday name. " + "If None, applies every day unless `date` is set." + ) + }, ) date: Optional[Date] = Field( default=None, - description=( - "Optional specific calendar date for the time window. Overrides `day_of_week` if set." - ), + json_schema_extra={ + "description": ( + "Optional specific calendar date for the time window. Overrides `day_of_week` if set." + ) + }, ) locale: Optional[str] = Field( default=None, - description=( - "Locale used to parse weekday names in `day_of_week` when given as string. " - "If not set, Pendulum's default locale is used. " - "Examples: 'en', 'de', 'fr', etc." - ), + json_schema_extra={ + "description": ( + "Locale used to parse weekday names in `day_of_week` when given as string. " + "If not set, Pendulum's default locale is used. " + "Examples: 'en', 'de', 'fr', etc." + ) + }, ) @field_validator("duration", mode="before") @@ -1160,7 +1171,8 @@ class TimeWindowSequence(BaseModel): """ windows: Optional[list[TimeWindow]] = Field( - default_factory=list, description="List of TimeWindow objects that make up this sequence." + default_factory=list, + json_schema_extra={"description": "List of TimeWindow objects that make up this sequence."}, ) @field_validator("windows") diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py index c905640..378c01c 100644 --- a/tests/test_pydantic.py +++ b/tests/test_pydantic.py @@ -17,10 +17,40 @@ from akkudoktoreos.utils.datetimeutil import DateTime, compare_datetimes, to_dat class PydanticTestModel(PydanticBaseModel): + """Minimal test model for exercising PydanticBaseModel helpers.""" + datetime_field: DateTime = Field( - ..., description="A datetime field with pendulum support." + ..., + description="A datetime field with pendulum support.", + json_schema_extra={"description": "A datetime field with pendulum support."}, + ) + + optional_field: Optional[str] = Field( + default=None, + # optional field with no description + ) + + # --------------------------------------------------------------------- + # Additional fields to support metadata-based testing + # --------------------------------------------------------------------- + + described_field: str = Field( + default="x", + description="A described string", + json_schema_extra={"description": "A described string"}, + ) + + deprecated_field: str = Field( + default="y", + description="A deprecated string field", + json_schema_extra={"deprecated": "Use new_field instead"}, + ) + + example_field: str = Field( + default="z", + description="An example-backed string field", + json_schema_extra={"examples": ["a", "b", "c"]}, ) - optional_field: Optional[str] = Field(default=None, description="An optional field.") class Address(PydanticBaseModel): @@ -377,6 +407,36 @@ class TestPydanticBaseModel: restored_model = PydanticTestModel.from_json(json_data) assert restored_model.datetime_field == dt + def test_field_extra_dict(self): + field = PydanticTestModel.model_fields["described_field"] + extra = PydanticTestModel._field_extra_dict(field) + assert isinstance(extra, dict) + assert extra.get("description") == "A described string" + + def test_field_description(self): + result = PydanticTestModel.field_description("described_field") + assert result == "A described string" + + def test_field_description_missing(self): + result = PydanticTestModel.field_description("optional_field") + assert result is None + + def test_field_deprecated(self): + result = PydanticTestModel.field_deprecated("deprecated_field") + assert result == "Use new_field instead" + + def test_field_deprecated_missing(self): + result = PydanticTestModel.field_deprecated("described_field") + assert result is None + + def test_field_examples(self): + result = PydanticTestModel.field_examples("example_field") + assert result == ["a", "b", "c"] + + def test_field_examples_missing(self): + result = PydanticTestModel.field_examples("optional_field") + assert result is None + class TestPydanticDateTimeData: def test_valid_list_lengths(self):