Review comments

This commit is contained in:
Dominique Lasserre 2025-01-24 21:14:37 +01:00
parent 56403fe053
commit 26762e5e93
10 changed files with 75 additions and 190 deletions

View File

@ -38,7 +38,7 @@ The configuration sources and their priorities are as follows:
### Runtime Config Updates ### Runtime Config Updates
The EOS configuration can be updated at runtime. Note that those updates are not persistet The EOS configuration can be updated at runtime. Note that those updates are not persistent
automatically. However it is possible to save the configuration to the `EOS configuration file`. automatically. However it is possible to save the configuration to the `EOS configuration file`.
Use the following endpoints to change the current runtime configuration: Use the following endpoints to change the current runtime configuration:

View File

@ -196,7 +196,7 @@ Prediction keys:
Configuration options: Configuration options:
- `prediction`: General prediction configuration. - `general`: General configuration.
- `latitude`: Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)" - `latitude`: Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)"
- `longitude`: Longitude in decimal degrees, within -180 to 180 (°) - `longitude`: Longitude in decimal degrees, within -180 to 180 (°)

View File

@ -106,7 +106,7 @@
"title": "BaseBatteryParameters", "title": "BaseBatteryParameters",
"type": "object" "type": "object"
}, },
"ConfigCommonSettings-Input": { "GeneralSettings-Input": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.", "description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": { "properties": {
"data_cache_subpath": { "data_cache_subpath": {
@ -185,10 +185,10 @@
"title": "Longitude" "title": "Longitude"
} }
}, },
"title": "ConfigCommonSettings", "title": "GeneralSettings",
"type": "object" "type": "object"
}, },
"ConfigCommonSettings-Output": { "GeneralSettings-Output": {
"description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.", "description": "Settings for common configuration.\n\nGeneral configuration to set directories of cache and output files and system location (latitude\nand longitude).\nValidators ensure each parameter is within a specified range. A computed property, `timezone`,\ndetermines the time zone based on latitude and longitude.\n\nAttributes:\n latitude (Optional[float]): Latitude in degrees, must be between -90 and 90.\n longitude (Optional[float]): Longitude in degrees, must be between -180 and 180.\n\nProperties:\n timezone (Optional[str]): Computed time zone string based on the specified latitude\n and longitude.\n\nValidators:\n validate_latitude (float): Ensures `latitude` is within the range -90 to 90.\n validate_longitude (float): Ensures `longitude` is within the range -180 to 180.",
"properties": { "properties": {
"config_file_path": { "config_file_path": {
@ -343,7 +343,7 @@
"config_folder_path", "config_folder_path",
"config_file_path" "config_file_path"
], ],
"title": "ConfigCommonSettings", "title": "GeneralSettings",
"type": "object" "type": "object"
}, },
"ConfigEOS": { "ConfigEOS": {
@ -359,7 +359,7 @@
"default": {} "default": {}
}, },
"general": { "general": {
"$ref": "#/components/schemas/ConfigCommonSettings-Output", "$ref": "#/components/schemas/GeneralSettings-Output",
"default": { "default": {
"config_file_path": "/home/user/.config/net.akkudoktoreos.net/EOS.config.json", "config_file_path": "/home/user/.config/net.akkudoktoreos.net/EOS.config.json",
"config_folder_path": "/home/user/.config/net.akkudoktoreos.net", "config_folder_path": "/home/user/.config/net.akkudoktoreos.net",
@ -2317,7 +2317,7 @@
"general": { "general": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/components/schemas/ConfigCommonSettings-Input" "$ref": "#/components/schemas/GeneralSettings-Input"
}, },
{ {
"type": "null" "type": "null"

View File

@ -11,7 +11,7 @@ from typing import Any, Union
from pydantic.fields import ComputedFieldInfo, FieldInfo from pydantic.fields import ComputedFieldInfo, FieldInfo
from pydantic_core import PydanticUndefined from pydantic_core import PydanticUndefined
from akkudoktoreos.config.config import ConfigCommonSettings, ConfigEOS, get_config from akkudoktoreos.config.config import ConfigEOS, GeneralSettings, get_config
from akkudoktoreos.core.logging import get_logger from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.utils.docs import get_model_structure_from_examples from akkudoktoreos.utils.docs import get_model_structure_from_examples
@ -250,10 +250,10 @@ def generate_config_md(config_eos: ConfigEOS) -> str:
str: The Markdown representation of the configuration spec. str: The Markdown representation of the configuration spec.
""" """
# Fix file path for general settings to not show local/test file path # Fix file path for general settings to not show local/test file path
ConfigCommonSettings._config_file_path = Path( GeneralSettings._config_file_path = Path(
"/home/user/.config/net.akkudoktoreos.net/EOS.config.json" "/home/user/.config/net.akkudoktoreos.net/EOS.config.json"
) )
ConfigCommonSettings._config_folder_path = config_eos.general.config_file_path.parent GeneralSettings._config_folder_path = config_eos.general.config_file_path.parent
markdown = "# Configuration Table\n\n" markdown = "# Configuration Table\n\n"

View File

@ -27,6 +27,7 @@ from pydantic_settings.sources import ConfigFileSourceMixin
# settings # settings
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import SingletonMixin from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.decorators import classproperty
from akkudoktoreos.core.logging import get_logger from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.logsettings import LoggingCommonSettings from akkudoktoreos.core.logsettings import LoggingCommonSettings
from akkudoktoreos.core.pydantic import merge_models from akkudoktoreos.core.pydantic import merge_models
@ -40,7 +41,7 @@ from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings
from akkudoktoreos.prediction.weather import WeatherCommonSettings from akkudoktoreos.prediction.weather import WeatherCommonSettings
from akkudoktoreos.server.server import ServerCommonSettings from akkudoktoreos.server.server import ServerCommonSettings
from akkudoktoreos.utils.datetimeutil import to_timezone from akkudoktoreos.utils.datetimeutil import to_timezone
from akkudoktoreos.utils.utils import UtilsCommonSettings, classproperty from akkudoktoreos.utils.utils import UtilsCommonSettings
logger = get_logger(__name__) logger = get_logger(__name__)
@ -63,7 +64,7 @@ def get_absolute_path(
return None return None
class ConfigCommonSettings(SettingsBaseModel): class GeneralSettings(SettingsBaseModel):
"""Settings for common configuration. """Settings for common configuration.
General configuration to set directories of cache and output files and system location (latitude General configuration to set directories of cache and output files and system location (latitude
@ -152,7 +153,7 @@ class SettingsEOS(BaseSettings):
Used by updating the configuration with specific settings only. Used by updating the configuration with specific settings only.
""" """
general: Optional[ConfigCommonSettings] = None general: Optional[GeneralSettings] = None
logging: Optional[LoggingCommonSettings] = None logging: Optional[LoggingCommonSettings] = None
devices: Optional[DevicesCommonSettings] = None devices: Optional[DevicesCommonSettings] = None
measurement: Optional[MeasurementCommonSettings] = None measurement: Optional[MeasurementCommonSettings] = None
@ -176,7 +177,7 @@ class SettingsEOSDefaults(SettingsEOS):
Used by ConfigEOS instance to make all fields available. Used by ConfigEOS instance to make all fields available.
""" """
general: ConfigCommonSettings = ConfigCommonSettings() general: GeneralSettings = GeneralSettings()
logging: LoggingCommonSettings = LoggingCommonSettings() logging: LoggingCommonSettings = LoggingCommonSettings()
devices: DevicesCommonSettings = DevicesCommonSettings() devices: DevicesCommonSettings = DevicesCommonSettings()
measurement: MeasurementCommonSettings = MeasurementCommonSettings() measurement: MeasurementCommonSettings = MeasurementCommonSettings()
@ -254,7 +255,7 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
"""Customizes the order and handling of settings sources for a Pydantic BaseSettings subclass. """Customizes the order and handling of settings sources for a Pydantic BaseSettings subclass.
This method determines the sources for application configuration settings, including This method determines the sources for application configuration settings, including
environment variables, dotenv files, JSON configuration files, and file secrets. environment variables, dotenv files and JSON configuration files.
It ensures that a default configuration file exists and creates one if necessary. It ensures that a default configuration file exists and creates one if necessary.
Args: Args:
@ -262,7 +263,7 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
init_settings (PydanticBaseSettingsSource): The initial settings source, typically passed at runtime. init_settings (PydanticBaseSettingsSource): The initial settings source, typically passed at runtime.
env_settings (PydanticBaseSettingsSource): Settings sourced from environment variables. env_settings (PydanticBaseSettingsSource): Settings sourced from environment variables.
dotenv_settings (PydanticBaseSettingsSource): Settings sourced from a dotenv file. dotenv_settings (PydanticBaseSettingsSource): Settings sourced from a dotenv file.
file_secret_settings (PydanticBaseSettingsSource): Settings sourced from secret files. file_secret_settings (PydanticBaseSettingsSource): Unused (needed for parent class interface).
Returns: Returns:
tuple[PydanticBaseSettingsSource, ...]: A tuple of settings sources in the order they should be applied. tuple[PydanticBaseSettingsSource, ...]: A tuple of settings sources in the order they should be applied.
@ -272,8 +273,8 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
2. If the configuration file does not exist, creates the directory (if needed) and attempts to copy a 2. If the configuration file does not exist, creates the directory (if needed) and attempts to copy a
default configuration file to the location. If the copy fails, uses the default configuration file directly. default configuration file to the location. If the copy fails, uses the default configuration file directly.
3. Creates a `JsonConfigSettingsSource` for both the configuration file and the default configuration file. 3. Creates a `JsonConfigSettingsSource` for both the configuration file and the default configuration file.
4. Updates class attributes `ConfigCommonSettings._config_folder_path` and 4. Updates class attributes `GeneralSettings._config_folder_path` and
`ConfigCommonSettings._config_file_path` to reflect the determined paths. `GeneralSettings._config_file_path` to reflect the determined paths.
5. Returns a tuple containing all provided and newly created settings sources in the desired order. 5. Returns a tuple containing all provided and newly created settings sources in the desired order.
Notes: Notes:
@ -295,15 +296,14 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
default_settings = JsonConfigSettingsSource( default_settings = JsonConfigSettingsSource(
settings_cls, json_file=cls.config_default_file_path settings_cls, json_file=cls.config_default_file_path
) )
ConfigCommonSettings._config_folder_path = config_dir GeneralSettings._config_folder_path = config_dir
ConfigCommonSettings._config_file_path = config_file GeneralSettings._config_file_path = config_file
return ( return (
init_settings, init_settings,
env_settings, env_settings,
dotenv_settings, dotenv_settings,
file_settings, file_settings,
file_secret_settings,
default_settings, default_settings,
) )

View File

@ -0,0 +1,42 @@
from typing import Any, Optional
from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__)
class classproperty(property):
"""A decorator to define a read-only property at the class level.
This class extends the built-in `property` to allow a method to be accessed
as a property on the class itself, rather than an instance. This is useful
when you want a property-like syntax for methods that depend on the class
rather than any instance of the class.
Example:
class MyClass:
_value = 42
@classproperty
def value(cls):
return cls._value
print(MyClass.value) # Outputs: 42
Methods:
__get__: Retrieves the value of the class property by calling the
decorated method on the class.
Parameters:
fget (Callable[[type], Any]): A method that takes the class as an
argument and returns a value.
Raises:
AssertionError: If `fget` is not defined when `__get__` is called.
"""
def __get__(self, _: Any, owner_cls: Optional[type[Any]] = None) -> Any:
if owner_cls is None:
return self
assert self.fget is not None
return self.fget(owner_cls)

View File

@ -39,153 +39,6 @@ class Devices(SingletonMixin, DevicesBase):
device.post_setup() device.post_setup()
# # Devices
# # TODO: Make devices class a container of device simulation providers.
# # Device simulations to be used are then enabled in the configuration.
# battery: ClassVar[Battery] = Battery(provider_id="GenericBattery")
# ev: ClassVar[Battery] = Battery(provider_id="GenericBEV")
# home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher")
# inverter: ClassVar[Inverter] = Inverter(
# self_consumption_predictor=SelfConsumptionProbabilityInterpolator,
# battery=battery,
# provider_id="GenericInverter",
# )
#
# def update_data(self) -> None:
# """Update device simulation data."""
# # Assure devices are set up
# self.battery.setup()
# self.ev.setup()
# self.home_appliance.setup()
# self.inverter.setup()
#
# # Pre-allocate arrays for the results, optimized for speed
# self.last_wh_pro_stunde = np.full((self.total_hours), np.nan)
# self.grid_export_wh_pro_stunde = np.full((self.total_hours), np.nan)
# self.grid_import_wh_pro_stunde = np.full((self.total_hours), np.nan)
# self.kosten_euro_pro_stunde = np.full((self.total_hours), np.nan)
# self.einnahmen_euro_pro_stunde = np.full((self.total_hours), np.nan)
# self.akku_soc_pro_stunde = np.full((self.total_hours), np.nan)
# self.eauto_soc_pro_stunde = np.full((self.total_hours), np.nan)
# self.verluste_wh_pro_stunde = np.full((self.total_hours), np.nan)
# self.home_appliance_wh_per_hour = np.full((self.total_hours), np.nan)
#
# # Set initial state
# simulation_step = to_duration("1 hour")
# if self.battery:
# self.akku_soc_pro_stunde[0] = self.battery.current_soc_percentage()
# if self.ev:
# self.eauto_soc_pro_stunde[0] = self.ev.current_soc_percentage()
#
# # Get predictions for full device simulation time range
# # gesamtlast[stunde]
# load_total_mean = self.prediction.key_to_array(
# "load_total_mean",
# start_datetime=self.start_datetime,
# end_datetime=self.end_datetime,
# interval=simulation_step,
# )
# # pv_prognose_wh[stunde]
# pvforecast_ac_power = self.prediction.key_to_array(
# "pvforecast_ac_power",
# start_datetime=self.start_datetime,
# end_datetime=self.end_datetime,
# interval=simulation_step,
# )
# # strompreis_euro_pro_wh[stunde]
# elecprice_marketprice_wh = self.prediction.key_to_array(
# "elecprice_marketprice_wh",
# start_datetime=self.start_datetime,
# end_datetime=self.end_datetime,
# interval=simulation_step,
# )
# # einspeiseverguetung_euro_pro_wh_arr[stunde]
# # TODO: Create prediction for einspeiseverguetung_euro_pro_wh_arr
# einspeiseverguetung_euro_pro_wh_arr = np.full((self.total_hours), 0.078)
#
# for stunde_since_now in range(0, self.total_hours):
# hour = self.start_datetime.hour + stunde_since_now
#
# # Accumulate loads and PV generation
# consumption = load_total_mean[stunde_since_now]
# self.verluste_wh_pro_stunde[stunde_since_now] = 0.0
#
# # Home appliances
# if self.home_appliance:
# ha_load = self.home_appliance.get_load_for_hour(hour)
# consumption += ha_load
# self.home_appliance_wh_per_hour[stunde_since_now] = ha_load
#
# # E-Auto handling
# if self.ev:
# if self.ev_charge_hours[hour] > 0:
# geladene_menge_eauto, verluste_eauto = self.ev.charge_energy(
# None, hour, relative_power=self.ev_charge_hours[hour]
# )
# consumption += geladene_menge_eauto
# self.verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto
# self.eauto_soc_pro_stunde[stunde_since_now] = self.ev.current_soc_percentage()
#
# # Process inverter logic
# grid_export, grid_import, losses, self_consumption = (0.0, 0.0, 0.0, 0.0)
# if self.battery:
# self.battery.set_charge_allowed_for_hour(self.dc_charge_hours[hour], hour)
# if self.inverter:
# generation = pvforecast_ac_power[hour]
# grid_export, grid_import, losses, self_consumption = self.inverter.process_energy(
# generation, consumption, hour
# )
#
# # AC PV Battery Charge
# if self.battery and self.ac_charge_hours[hour] > 0.0:
# self.battery.set_charge_allowed_for_hour(1, hour)
# geladene_menge, verluste_wh = self.battery.charge_energy(
# None, hour, relative_power=self.ac_charge_hours[hour]
# )
# # print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.battery.current_soc_percentage())
# consumption += geladene_menge
# grid_import += geladene_menge
# self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh
#
# self.grid_export_wh_pro_stunde[stunde_since_now] = grid_export
# self.grid_import_wh_pro_stunde[stunde_since_now] = grid_import
# self.verluste_wh_pro_stunde[stunde_since_now] += losses
# self.last_wh_pro_stunde[stunde_since_now] = consumption
#
# # Financial calculations
# self.kosten_euro_pro_stunde[stunde_since_now] = (
# grid_import * self.strompreis_euro_pro_wh[hour]
# )
# self.einnahmen_euro_pro_stunde[stunde_since_now] = (
# grid_export * self.einspeiseverguetung_euro_pro_wh_arr[hour]
# )
#
# # battery SOC tracking
# if self.battery:
# self.akku_soc_pro_stunde[stunde_since_now] = self.battery.current_soc_percentage()
# else:
# self.akku_soc_pro_stunde[stunde_since_now] = 0.0
#
# def report_dict(self) -> Dict[str, Any]:
# """Provides devices simulation output as a dictionary."""
# out: Dict[str, Optional[Union[np.ndarray, float]]] = {
# "Last_Wh_pro_Stunde": self.last_wh_pro_stunde,
# "grid_export_Wh_pro_Stunde": self.grid_export_wh_pro_stunde,
# "grid_import_Wh_pro_Stunde": self.grid_import_wh_pro_stunde,
# "Kosten_Euro_pro_Stunde": self.kosten_euro_pro_stunde,
# "akku_soc_pro_stunde": self.akku_soc_pro_stunde,
# "Einnahmen_Euro_pro_Stunde": self.einnahmen_euro_pro_stunde,
# "Gesamtbilanz_Euro": self.total_balance_euro,
# "EAuto_SoC_pro_Stunde": self.eauto_soc_pro_stunde,
# "Gesamteinnahmen_Euro": self.total_revenues_euro,
# "Gesamtkosten_Euro": self.total_costs_euro,
# "Verluste_Pro_Stunde": self.verluste_wh_pro_stunde,
# "Gesamt_Verluste": self.total_losses_wh,
# "Home_appliance_wh_per_hour": self.home_appliance_wh_per_hour,
# }
# return out
# Initialize the Devices simulation, it is a singleton. # Initialize the Devices simulation, it is a singleton.
devices = Devices() devices = Devices()

View File

@ -147,14 +147,12 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence):
"""Provides measurement key for given name and topic.""" """Provides measurement key for given name and topic."""
topic = topic.lower() topic = topic.lower()
print(self.topics)
if topic not in self.topics: if topic not in self.topics:
return None return None
topic_keys = [ topic_keys = [
key for key in self.config.measurement.model_fields.keys() if key.startswith(topic) key for key in self.config.measurement.model_fields.keys() if key.startswith(topic)
] ]
print(topic_keys)
key = None key = None
if topic == "load": if topic == "load":
for config_key in topic_keys: for config_key in topic_keys:

View File

@ -1,5 +1,5 @@
import json import json
from typing import Any, Optional from typing import Any
import numpy as np import numpy as np
@ -9,14 +9,6 @@ from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
class classproperty(property):
def __get__(self, _: Any, owner_cls: Optional[type[Any]] = None) -> Any:
if owner_cls is None:
return self
assert self.fget is not None
return self.fget(owner_cls)
class UtilsCommonSettings(SettingsBaseModel): class UtilsCommonSettings(SettingsBaseModel):
"""Utils Configuration.""" """Utils Configuration."""

View File

@ -5,7 +5,7 @@ from unittest.mock import patch
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
from akkudoktoreos.config.config import ConfigCommonSettings, ConfigEOS from akkudoktoreos.config.config import ConfigEOS, GeneralSettings
from akkudoktoreos.core.logging import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -159,8 +159,8 @@ def test_config_copy(config_eos, monkeypatch):
], ],
) )
def test_config_common_settings_valid(latitude, longitude, expected_timezone): def test_config_common_settings_valid(latitude, longitude, expected_timezone):
"""Test valid settings for ConfigCommonSettings.""" """Test valid settings for GeneralSettings."""
general_settings = ConfigCommonSettings( general_settings = GeneralSettings(
latitude=latitude, latitude=latitude,
longitude=longitude, longitude=longitude,
) )
@ -184,30 +184,30 @@ def test_config_common_settings_invalid(field_name, invalid_value, expected_erro
"latitude": 40.7128, "latitude": 40.7128,
"longitude": -74.0060, "longitude": -74.0060,
} }
assert ConfigCommonSettings(**valid_data) is not None assert GeneralSettings(**valid_data) is not None
valid_data[field_name] = invalid_value valid_data[field_name] = invalid_value
with pytest.raises(ValidationError, match=expected_error): with pytest.raises(ValidationError, match=expected_error):
ConfigCommonSettings(**valid_data) GeneralSettings(**valid_data)
def test_config_common_settings_no_location(): def test_config_common_settings_no_location():
"""Test that timezone is None when latitude and longitude are not provided.""" """Test that timezone is None when latitude and longitude are not provided."""
settings = ConfigCommonSettings(latitude=None, longitude=None) settings = GeneralSettings(latitude=None, longitude=None)
assert settings.timezone is None assert settings.timezone is None
def test_config_common_settings_with_location(): def test_config_common_settings_with_location():
"""Test that timezone is correctly computed when latitude and longitude are provided.""" """Test that timezone is correctly computed when latitude and longitude are provided."""
settings = ConfigCommonSettings(latitude=34.0522, longitude=-118.2437) settings = GeneralSettings(latitude=34.0522, longitude=-118.2437)
assert settings.timezone == "America/Los_Angeles" assert settings.timezone == "America/Los_Angeles"
def test_config_common_settings_timezone_none_when_coordinates_missing(): def test_config_common_settings_timezone_none_when_coordinates_missing():
"""Test that timezone is None when latitude or longitude is missing.""" """Test that timezone is None when latitude or longitude is missing."""
config_no_latitude = ConfigCommonSettings(latitude=None, longitude=-74.0060) config_no_latitude = GeneralSettings(latitude=None, longitude=-74.0060)
config_no_longitude = ConfigCommonSettings(latitude=40.7128, longitude=None) config_no_longitude = GeneralSettings(latitude=40.7128, longitude=None)
config_no_coords = ConfigCommonSettings(latitude=None, longitude=None) config_no_coords = GeneralSettings(latitude=None, longitude=None)
assert config_no_latitude.timezone is None assert config_no_latitude.timezone is None
assert config_no_longitude.timezone is None assert config_no_longitude.timezone is None