Skip to content

Commit

Permalink
Add requested changes #225
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickjaigner committed Jul 15, 2024
1 parent 43100b7 commit 3a4c0ff
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 70 deletions.
1 change: 1 addition & 0 deletions config/thingsboard.default.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"host": "0.0.0.0",
"access_token": "...",
"seconds_per_publish_interval": 60,
"ca_cert": null
}
6 changes: 0 additions & 6 deletions package-lock.json

This file was deleted.

51 changes: 29 additions & 22 deletions packages/core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ def run() -> None:
according to the config.
"""

logger.info(f"Starting mainloop inside process with process ID {os.getpid()}")
logger.info(
f"Starting mainloop inside process with process ID {os.getpid()}"
)

# Loop until a valid config has been found. Without
# an invalid config, the mainloop cannot initialize
Expand All @@ -89,8 +91,8 @@ def run() -> None:
break
except ValueError as e:
logger.error(
"Invalid config, waiting 10 seconds: "
+ str(e).replace("Config is invalid:", "")
"Invalid config, waiting 10 seconds: " +
str(e).replace("Config is invalid:", "")
)
time.sleep(10)
except Exception as e:
Expand All @@ -104,25 +106,29 @@ def run() -> None:
# these modules will be executed one by one in each
# mainloop iteration
logger.info("Initializing mainloop modules")
mainloop_modules: list[
tuple[
Literal[
"measurement-conditions",
"enclosure-control",
"sun-tracking",
"opus-measurement",
"system-checks",
],
Callable[[types.Config], None],
]
] = [
mainloop_modules: list[tuple[
Literal[
"measurement-conditions",
"enclosure-control",
"sun-tracking",
"opus-measurement",
"system-checks",
],
Callable[[types.Config], None],
]] = [
(
"measurement-conditions",
modules.measurement_conditions.MeasurementConditions(config).run,
),
("enclosure-control", modules.enclosure_control.EnclosureControl(config).run),
(
"enclosure-control",
modules.enclosure_control.EnclosureControl(config).run
),
("sun-tracking", modules.sun_tracking.SunTracking(config).run),
("opus-measurement", modules.opus_measurement.OpusMeasurement(config).run),
(
"opus-measurement",
modules.opus_measurement.OpusMeasurement(config).run
),
("system-checks", modules.system_checks.SystemChecks(config).run),
]

Expand All @@ -133,9 +139,10 @@ def run() -> None:
logger.info("Initializing threads")
helios_thread_instance = threads.HeliosThread(config)
upload_thread_instance = threads.UploadThread(config)
thingsboard_instance = threads.ThingsBoardThread(config)
thingsboard_thread_instance = threads.ThingsBoardThread(config)

current_exceptions = interfaces.StateInterface.load_state().current_exceptions or []
current_exceptions = interfaces.StateInterface.load_state(
).current_exceptions or []

logger.info("Removing temporary state from previous runs")
interfaces.StateInterface.update_state(
Expand Down Expand Up @@ -173,8 +180,8 @@ def _graceful_teardown(*args: Any) -> None:
config = types.Config.load()
except ValueError as e:
logger.error(
"Invalid config, waiting 10 seconds: "
+ str(e).replace("Config is invalid:", "")
"Invalid config, waiting 10 seconds: " +
str(e).replace("Config is invalid:", "")
)
time.sleep(10)
continue
Expand All @@ -188,7 +195,7 @@ def _graceful_teardown(*args: Any) -> None:
# possibly (re)start each thread
helios_thread_instance.update_thread_state(config)
upload_thread_instance.update_thread_state(config)
thingsboard_instance.update_thread_state(config)
thingsboard_thread_instance.update_thread_state(config)

if config.general.test_mode:
logger.info("pyra-core in test mode")
Expand Down
66 changes: 43 additions & 23 deletions packages/core/threads/thingsboard_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@


class ThingsBoardThread(AbstractThread):

@staticmethod
def should_be_running(config: types.Config) -> bool:
"""Based on the config, should the thread be running or not?"""
Expand Down Expand Up @@ -42,7 +41,7 @@ def main(headless: bool = False) -> None:
)

# connect to MQTT broker
if config.thingsboard.ca_cert:
if config.thingsboard.ca_cert is not None:
client.connect(tls=True, ca_certs=config.thingsboard.ca_cert)
else:
client.connect()
Expand All @@ -66,26 +65,47 @@ def main(headless: bool = False) -> None:
# read latest state file
current_state = interfaces.StateInterface.load_state()
state: Dict[str, Optional[Union[str, bool, int, float]]] = {
"last_updated": str(current_state.last_updated),
"helios_indicates_good_conditions": current_state.helios_indicates_good_conditions,
"measurements_should_be_running": current_state.measurements_should_be_running,
"memory_usage": current_state.operating_system_state.memory_usage,
"filled_disk_space_fraction": current_state.operating_system_state.filled_disk_space_fraction,
"fan_speed": current_state.plc_state.actors.fan_speed,
"current_angle": current_state.plc_state.actors.current_angle,
"auto_temp_mode": current_state.plc_state.control.auto_temp_mode,
"manual_control": current_state.plc_state.control.manual_control,
"manual_temp_mode": current_state.plc_state.control.manual_temp_mode,
"sync_to_tracker": current_state.plc_state.control.sync_to_tracker,
"humidity": current_state.plc_state.sensors.humidity,
"temperature": current_state.plc_state.sensors.temperature,
"cover_closed": current_state.plc_state.state.cover_closed,
"motor_failed": current_state.plc_state.state.motor_failed,
"rain": current_state.plc_state.state.rain,
"reset_needed": current_state.plc_state.state.reset_needed,
"ups_alert": current_state.plc_state.state.ups_alert,
"heater": current_state.plc_state.power.heater,
"spectrometer": current_state.plc_state.power.spectrometer,
"state_file_last_updated":
str(current_state.last_updated),
"helios_indicates_good_conditions":
current_state.helios_indicates_good_conditions,
"measurements_should_be_running":
current_state.measurements_should_be_running,
"os_memory_usage":
current_state.operating_system_state.memory_usage,
"os_filled_disk_space_fraction":
current_state.operating_system_state.
filled_disk_space_fraction,
"enclosure_actor_fan_speed":
current_state.plc_state.actors.fan_speed,
"enclosure_actor_current_angle":
current_state.plc_state.actors.current_angle,
"enclosure_control_auto_temp_mode":
current_state.plc_state.control.auto_temp_mode,
"enclosure_control_manual_control":
current_state.plc_state.control.manual_control,
"enclosure_control_manual_temp_mode":
current_state.plc_state.control.manual_temp_mode,
"enclosure_control_sync_to_tracker":
current_state.plc_state.control.sync_to_tracker,
"enclosure_sensor_humidity":
current_state.plc_state.sensors.humidity,
"enclosure_sensor_temperature":
current_state.plc_state.sensors.temperature,
"enclosure_state_cover_closed":
current_state.plc_state.state.cover_closed,
"enclosure_state_motor_failed":
current_state.plc_state.state.motor_failed,
"enclosure_state_rain":
current_state.plc_state.state.rain,
"enclosure_state_reset_needed":
current_state.plc_state.state.reset_needed,
"enclosure_state_ups_alert":
current_state.plc_state.state.ups_alert,
"enclosure_power_heater":
current_state.plc_state.power.heater,
"enclosure_power_spectrometer":
current_state.plc_state.power.spectrometer,
}

telemetry_with_ts = {
Expand All @@ -101,4 +121,4 @@ def main(headless: bool = False) -> None:
logger.exception(e)
logger.info("Failed to publish last telemetry data.")

time.sleep(config.general.seconds_per_core_interval)
time.sleep(config.thingsboard.seconds_per_publish_interval or 60)
57 changes: 38 additions & 19 deletions packages/core/types/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import pydantic
import tum_esm_utils

_PROJECT_DIR = tum_esm_utils.files.get_parent_dir_path(__file__, current_depth=4)
_PROJECT_DIR = tum_esm_utils.files.get_parent_dir_path(
__file__, current_depth=4
)
_CONFIG_FILE_PATH = os.path.join(_PROJECT_DIR, "config", "config.json")
_CONFIG_LOCK_PATH = os.path.join(_PROJECT_DIR, "config", ".config.lock")

Expand All @@ -22,11 +24,9 @@ class StrictFilePath(pydantic.RootModel[str]):
@pydantic.field_validator("root")
@classmethod
def path_should_exist(cls, v: str, info: pydantic.ValidationInfo) -> str:
ignore_path_existence = (
(info.context.get("ignore-path-existence") == True)
if isinstance(info.context, dict)
else False
)
ignore_path_existence = ((
info.context.get("ignore-path-existence") == True
) if isinstance(info.context, dict) else False)
if (not ignore_path_existence) and (not os.path.isfile(v)):
raise ValueError("File does not exist")
return v
Expand All @@ -38,11 +38,9 @@ class StrictDirectoryPath(pydantic.RootModel[str]):
@pydantic.field_validator("root")
@classmethod
def path_should_exist(cls, v: str, info: pydantic.ValidationInfo) -> str:
ignore_path_existence = (
(info.context.get("ignore-path-existence") == True)
if isinstance(info.context, dict)
else False
)
ignore_path_existence = ((
info.context.get("ignore-path-existence") == True
) if isinstance(info.context, dict) else False)
if (not ignore_path_existence) and (not os.path.isdir(v)):
raise ValueError("Directory does not exist")
return v
Expand Down Expand Up @@ -81,7 +79,9 @@ class PartialGeneralConfig(StricterBaseModel):
"""Like `GeneralConfig`, but all fields are optional."""

version: Literal["4.1.3"] = "4.1.3"
seconds_per_core_interval: Optional[float] = pydantic.Field(None, ge=5, le=600)
seconds_per_core_interval: Optional[float] = pydantic.Field(
None, ge=5, le=600
)
test_mode: Optional[bool] = None
station_id: Optional[str] = None
min_sun_elevation: Optional[float] = pydantic.Field(None, ge=0, le=90)
Expand Down Expand Up @@ -260,12 +260,16 @@ class PartialUploadConfig(StricterBaseModel):
class ThingsBoardConfig(StricterBaseModel):
host: str
access_token: int
seconds_per_publish_interval: int = pydantic.Field(..., ge=30, le=999999)
ca_cert: Optional[str]


class PartialThingsBoardConfig(StricterBaseModel):
host: Optional[str] = None
access_token: Optional[int] = None
seconds_per_publish_interval: Optional[int] = pydantic.Field(
None, ge=30, le=999999
)
ca_cert: Optional[str] = None


Expand Down Expand Up @@ -310,20 +314,26 @@ def load(
if isinstance(config_object, dict):
return Config.model_validate(
config_object,
context={"ignore-path-existence": ignore_path_existence},
context={
"ignore-path-existence": ignore_path_existence
},
)
else:
return Config.model_validate_json(
config_object,
context={"ignore-path-existence": ignore_path_existence},
context={
"ignore-path-existence": ignore_path_existence
},
)
else:

def _read() -> Config:
with open(_CONFIG_FILE_PATH) as f:
return Config.model_validate_json(
f.read(),
context={"ignore-path-existence": ignore_path_existence},
context={
"ignore-path-existence": ignore_path_existence
},
)

if with_filelock:
Expand All @@ -337,7 +347,9 @@ def _read() -> Config:
location = ".".join([str(err) for err in er["loc"]])
message = er["msg"]
value = er["input"]
pretty_errors.append(f"Error in {location}: {message} (value: {value})")
pretty_errors.append(
f"Error in {location}: {message} (value: {value})"
)

# the "from None" suppresses the pydantic exception
raise ValueError(
Expand All @@ -355,7 +367,9 @@ def dump(self, with_filelock: bool = True) -> None:

@staticmethod
@contextlib.contextmanager
@tum_esm_utils.decorators.with_filelock(lockfile_path=_CONFIG_LOCK_PATH, timeout=5)
@tum_esm_utils.decorators.with_filelock(
lockfile_path=_CONFIG_LOCK_PATH, timeout=5
)
def update_in_context() -> Generator[Config, None, None]:
"""Update the confug file in a context manager.
Expand Down Expand Up @@ -396,7 +410,10 @@ class PartialConfig(StricterBaseModel):
thingsboard: Optional[PartialThingsBoardConfig] = None

@staticmethod
def load(config_object: str, ignore_path_existence: bool = False) -> PartialConfig:
def load(
config_object: str,
ignore_path_existence: bool = False
) -> PartialConfig:
"""Load a partial config file.
Args:
Expand All @@ -422,7 +439,9 @@ def load(config_object: str, ignore_path_existence: bool = False) -> PartialConf
location = ".".join([str(err) for err in er["loc"]])
message = er["msg"]
value = er["input"]
pretty_errors.append(f"Error in {location}: {message} (value: {value})")
pretty_errors.append(
f"Error in {location}: {message} (value: {value})"
)

# the "from None" suppresses the pydantic exception
raise ValueError(
Expand Down

0 comments on commit 3a4c0ff

Please sign in to comment.