diff --git a/src/gjk/enums/change_aquastat_control.py b/src/gjk/enums/change_aquastat_control.py index 484e97e..78b06ba 100644 --- a/src/gjk/enums/change_aquastat_control.py +++ b/src/gjk/enums/change_aquastat_control.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/enums/change_heat_pump_control.py b/src/gjk/enums/change_heat_pump_control.py index 5e19834..10fb6d4 100644 --- a/src/gjk/enums/change_heat_pump_control.py +++ b/src/gjk/enums/change_heat_pump_control.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/enums/change_heatcall_source.py b/src/gjk/enums/change_heatcall_source.py index 897b2bf..9172222 100644 --- a/src/gjk/enums/change_heatcall_source.py +++ b/src/gjk/enums/change_heatcall_source.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/enums/change_lg_operating_mode.py b/src/gjk/enums/change_lg_operating_mode.py index 8223e43..4f7badc 100644 --- a/src/gjk/enums/change_lg_operating_mode.py +++ b/src/gjk/enums/change_lg_operating_mode.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/enums/change_primary_pump_control.py b/src/gjk/enums/change_primary_pump_control.py index 2fb2b5f..bbb661b 100644 --- a/src/gjk/enums/change_primary_pump_control.py +++ b/src/gjk/enums/change_primary_pump_control.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/enums/change_primary_pump_state.py b/src/gjk/enums/change_primary_pump_state.py index 45b3c9c..213f714 100644 --- a/src/gjk/enums/change_primary_pump_state.py +++ b/src/gjk/enums/change_primary_pump_state.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/enums/change_relay_pin.py b/src/gjk/enums/change_relay_pin.py index db52d5f..1324bb0 100644 --- a/src/gjk/enums/change_relay_pin.py +++ b/src/gjk/enums/change_relay_pin.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/enums/change_valve_state.py b/src/gjk/enums/change_valve_state.py index 874ff5a..7405726 100644 --- a/src/gjk/enums/change_valve_state.py +++ b/src/gjk/enums/change_valve_state.py @@ -1,4 +1,3 @@ - # Literal Enum: # - no additional values can be added over time. # - Sent as-is, not in hex symbol diff --git a/src/gjk/types/fsm_atomic_report.py b/src/gjk/types/fsm_atomic_report.py index 1ec6809..84b581c 100644 --- a/src/gjk/types/fsm_atomic_report.py +++ b/src/gjk/types/fsm_atomic_report.py @@ -112,7 +112,7 @@ def check_axiom_3(self) -> Self: elif self.event_type or self.event or self.from_state or self.to_state: raise ValueError( "ReportType is NOT event => EventType, Event, FromState, ToState do not exist" - ) + ) return self @model_validator(mode="before") diff --git a/src/gjk/types/gridworks_event_gt_sh_status.py b/src/gjk/types/gridworks_event_gt_sh_status.py index a1f4c6a..35f41b3 100644 --- a/src/gjk/types/gridworks_event_gt_sh_status.py +++ b/src/gjk/types/gridworks_event_gt_sh_status.py @@ -1,11 +1,11 @@ """Type gridworks.event.gt.sh.status, version 000""" +import copy import json -import logging from typing import Any, Dict, Literal from gw.errors import GwTypeError -from gw.utils import is_pascal_case, snake_to_pascal +from gw.utils import recursively_pascal, snake_to_pascal from pydantic import ( BaseModel, ConfigDict, @@ -14,6 +14,7 @@ ) from typing_extensions import Self +from gjk.enums import TelemetryName from gjk.type_helpers.property_format import ( LeftRightDot, ReallyAnInt, @@ -21,12 +22,6 @@ ) from gjk.types.gt_sh_status import GtShStatus -LOG_FORMAT = ( - "%(levelname) -10s %(asctime)s %(name) -30s %(funcName) " - "-35s %(lineno) -5d: %(message)s" -) -LOGGER = logging.getLogger(__name__) - class GridworksEventGtShStatus(BaseModel): """ @@ -55,7 +50,12 @@ def check_axiom_1(self) -> Self: Axiom 1: SCADA time consistency. SlotStartS + ReportingPeriodS < MessageCreatedS (which is TimeNS / 10**9) """ - # Implement check for axiom 1" + # a = self.status.slot_start_unix_s + self.status.reporting_period_s + # b = self.time_n_s / 10**9 + # if a > b: + # raise ValueError( + # f"slot_start + reporting_period was larger than message created time!: {self.message_id}" + # ) return self @model_validator(mode="after") @@ -64,16 +64,71 @@ def check_axiom_2(self) -> Self: Axiom 2: Src is Status.FromGNodeAlias and MessageId matches Status.StatusUid. Src == Status.FromGNodeAlias """ - # Implement check for axiom 2" + if self.src != self.status.from_g_node_alias: + raise ValueError( + f"self.src <{self.src}> must be status.from_g_node_alias <{self.status.from_g_node_alias}>" + ) + + if self.message_id != self.status.status_uid: + raise ValueError( + f"message_id <{self.message_id}> must be status.status_uid <{self.status.status_uid}>" + ) + return self + @classmethod + def first_season_fix(cls, d: dict) -> dict: + """ + Makes key "status" -> "Status", following the rule that + all GridWorks types must have PascalCase keys + """ + + d2 = copy.deepcopy(d) + if "status" in d2.keys(): + d2["Status"] = d2["status"] + del d2["status"] + if "Status" not in d2.keys(): + raise GwTypeError(f"dict missing Status: <{d2}>") + + status = d2["Status"] + + # replace values with symbols for TelemetryName in SimpleTelemetryList + simple_list = status["SimpleTelemetryList"] + for simple in simple_list: + if "TelemetryName" not in simple.keys(): + raise Exception( + f"simple does not have TelemetryName in keys! simple.key()): <{simple.keys()}>" + ) + simple["TelemetryNameGtEnumSymbol"] = TelemetryName.value_to_symbol( + simple["TelemetryName"] + ) + del simple["TelemetryName"] + status["SimpleTelemetryList"] = simple_list + + # replace values with symbols for TelemetryName in MultipurposeTelemetryList + multi_list = status["MultipurposeTelemetryList"] + for multi in multi_list: + multi["TelemetryNameGtEnumSymbol"] = TelemetryName.value_to_symbol( + multi["TelemetryName"] + ) + del multi["TelemetryName"] + status["MultipurposeTelemetryList"] = multi_list + + orig_message_id = d2["MessageId"] + if orig_message_id != status["StatusUid"]: + d2["MessageId"] = status["StatusUid"] + + d2["Status"] = status + d2["Version"] = "000" + return d2 + @classmethod def from_dict(cls, d: dict) -> "GridworksEventGtShStatus": - for key in d: - if not is_pascal_case(key): - raise GwTypeError(f"Key '{key}' is not PascalCase") + d2 = cls.first_season_fix(d) + if not recursively_pascal(d2): + raise GwTypeError(f"Not recursively PascalCase: {d}") try: - t = cls(**d) + t = cls(**d2) except ValidationError as e: raise GwTypeError(f"Pydantic validation error: {e}") from e return t diff --git a/src/gjk/types/gridworks_event_snapshot_spaceheat.py b/src/gjk/types/gridworks_event_snapshot_spaceheat.py index a9aca9e..96de711 100644 --- a/src/gjk/types/gridworks_event_snapshot_spaceheat.py +++ b/src/gjk/types/gridworks_event_snapshot_spaceheat.py @@ -1,13 +1,15 @@ """Type gridworks.event.snapshot.spaceheat, version 000""" +import copy import json import logging from typing import Any, Dict, Literal from gw.errors import GwTypeError -from gw.utils import is_pascal_case, snake_to_pascal +from gw.utils import recursively_pascal, snake_to_pascal from pydantic import BaseModel, ConfigDict, ValidationError +from gjk.enums import TelemetryName from gjk.type_helpers.property_format import ( LeftRightDot, ReallyAnInt, @@ -45,13 +47,48 @@ class GridworksEventSnapshotSpaceheat(BaseModel): populate_by_name=True, ) + @classmethod + def first_season_fix(cls, d: dict[str, Any]) -> dict[str, Any]: + """ + Makes key "snap" -> "Snap", following the rule that + all GridWorks types must have PascalCase keys + """ + + d2 = copy.deepcopy(d) + + if "snap" in d2.keys(): + d2["Snap"] = d2["snap"] + del d2["snap"] + + if "Snap" not in d2.keys(): + raise GwTypeError(f"dict missing Snap: <{d2.keys()}>") + + if "Snapshot" not in d2["Snap"].keys(): + raise GwTypeError(f"dict['Snap'] missing Snapshot: <{d2['Snap'].keys()}>") + + snapshot = d2["Snap"]["Snapshot"] + # replace values with symbols for TelemetryName in SimpleTelemetryList + if "TelemetryNameList" not in snapshot.keys(): + raise Exception( + f"Snapshot does not have TelemetryNameList in keys! simple.key()): <{snapshot.keys()}>" + ) + telemetry_name_list = snapshot["TelemetryNameList"] + new_list = [] + for tn in telemetry_name_list: + new_list.append(TelemetryName.value_to_symbol(tn)) + snapshot["TelemetryNameList"] = new_list + + d2["Snap"]["Snapshot"] = snapshot + d2["Version"] = "000" + return d2 + @classmethod def from_dict(cls, d: dict) -> "GridworksEventSnapshotSpaceheat": - for key in d: - if not is_pascal_case(key): - raise GwTypeError(f"Key '{key}' is not PascalCase") + d2 = cls.first_season_fix(d) + if not recursively_pascal(d2): + raise GwTypeError(f"Not recursively PascalCase: {d2}") try: - t = cls(**d) + t = cls(**d2) except ValidationError as e: raise GwTypeError(f"Pydantic validation error: {e}") from e return t diff --git a/tests/enums/change_primary_pump_control_test.py b/tests/enums/change_primary_pump_control_test.py index dbed6e8..e3e63ee 100644 --- a/tests/enums/change_primary_pump_control_test.py +++ b/tests/enums/change_primary_pump_control_test.py @@ -11,5 +11,7 @@ def test_change_primary_pump_control() -> None: "SwitchToHeatPump", } - assert ChangePrimaryPumpControl.default() == ChangePrimaryPumpControl.SwitchToHeatPump + assert ( + ChangePrimaryPumpControl.default() == ChangePrimaryPumpControl.SwitchToHeatPump + ) assert ChangePrimaryPumpControl.enum_name() == "change.primary.pump.control" diff --git a/tests/types/test_fsm_atomic_report.py b/tests/types/test_fsm_atomic_report.py index 8f9fb87..7030154 100644 --- a/tests/types/test_fsm_atomic_report.py +++ b/tests/types/test_fsm_atomic_report.py @@ -1,11 +1,5 @@ """Tests fsm.atomic.report, version 000""" -from gjk.enums import ( - FsmActionType, - FsmEventType, - FsmName, - FsmReportType, -) from gjk.types import FsmAtomicReport @@ -33,4 +27,3 @@ def test_fsm_atomic_report_generated() -> None: del d2["ActionType"] d2["ActionTypeGtEnumSymbol"] = "00000000" assert t == FsmAtomicReport.from_dict(d2) - diff --git a/tests/types/test_fsm_event.py b/tests/types/test_fsm_event.py index 7755b8a..b1732d3 100644 --- a/tests/types/test_fsm_event.py +++ b/tests/types/test_fsm_event.py @@ -22,4 +22,3 @@ def test_fsm_event_generated() -> None: del d2["EventType"] d2["EventTypeGtEnumSymbol"] = "c234ee7a" assert t == FsmEvent.from_dict(d2) -