Skip to content

Commit

Permalink
ShNodes in layout.lite; updated scada params
Browse files Browse the repository at this point in the history
  • Loading branch information
jessicamillar committed Nov 28, 2024
1 parent e65ddf3 commit a660509
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 24 deletions.
4 changes: 4 additions & 0 deletions src/gwproto/data_classes/sh_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ def __repr__(self) -> str:
@property
def has_actor(self) -> bool:
return self.actor_class != ActorClass.NoActor

def to_gt(self) -> SpaceheatNodeGt:
# Copy the current instance excluding the extra fields
return SpaceheatNodeGt(**self.model_dump(exclude={"component"}))
2 changes: 2 additions & 0 deletions src/gwproto/named_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from gwproto.named_types.fsm_full_report import FsmFullReport
from gwproto.named_types.fsm_trigger_from_atn import FsmTriggerFromAtn
from gwproto.named_types.go_dormant import GoDormant
from gwproto.named_types.ha1_params import Ha1Params
from gwproto.named_types.heartbeat_b import HeartbeatB
from gwproto.named_types.hubitat_component_gt import HubitatComponentGt
from gwproto.named_types.hubitat_poller_component_gt import HubitatPollerComponentGt
Expand Down Expand Up @@ -84,6 +85,7 @@
"FsmFullReport",
"FsmTriggerFromAtn",
"GoDormant",
"Ha1Params",
"HeartbeatB",
"HubitatComponentGt",
"HubitatPollerComponentGt",
Expand Down
19 changes: 19 additions & 0 deletions src/gwproto/named_types/ha1_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Type ha1.params, version 000"""

from typing import Literal

from pydantic import BaseModel, StrictInt


class Ha1Params(BaseModel):
AlphaTimes10: StrictInt
BetaTimes100: StrictInt
GammaEx6: StrictInt
IntermediatePowerKw: float
IntermediateRswtF: StrictInt
DdPowerKw: float
DdRswtF: StrictInt
DdDeltaTF: StrictInt
HpMaxKwTh: float
TypeName: Literal["ha1.params"] = "ha1.params"
Version: Literal["000"] = "000"
67 changes: 57 additions & 10 deletions src/gwproto/named_types/layout_lite.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Type layout.lite, version 000"""
"""Type layout.lite, version 001"""

from typing import List, Literal

from pydantic import BaseModel, PositiveInt
from pydantic import BaseModel, PositiveInt, model_validator
from typing_extensions import Self

from gwproto.enums import ActorClass
from gwproto.named_types.data_channel_gt import DataChannelGt
from gwproto.named_types.pico_flow_module_component_gt import PicoFlowModuleComponentGt
from gwproto.named_types.pico_tank_module_component_gt import PicoTankModuleComponentGt
from gwproto.named_types.spaceheat_node_gt import SpaceheatNodeGt
from gwproto.property_format import (
LeftRightDotStr,
UTCMilliseconds,
Expand All @@ -15,22 +18,66 @@


class LayoutLite(BaseModel):
"""
Layout Lite.
A light-weight version of the layout for a Spaceheat Node, with key parameters about how
the SCADA operates.
"""

FromGNodeAlias: LeftRightDotStr
FromGNodeInstanceId: UUID4Str
MessageCreatedMs: UTCMilliseconds
MessageId: UUID4Str
Strategy: str
ZoneList: List[str]
TotalStoreTanks: PositiveInt
ShNodes: List[SpaceheatNodeGt]
DataChannels: List[DataChannelGt]
TankModuleComponents: List[PicoTankModuleComponentGt]
FlowModuleComponents: List[PicoFlowModuleComponentGt]
TypeName: Literal["layout.lite"] = "layout.lite"
Version: Literal["000"] = "000"
Version: Literal["001"] = "001"

@model_validator(mode="after")
def check_axiom_1(self) -> Self:
"""
Axiom 1: Dc Node Consistency. Every AboutNodeName and CapturedByNodeName in a
DataChannel belongs to an ShNode, and in addition every CapturedByNodeName does
not have ActorClass NoActor.
"""
for dc in self.DataChannels:
if dc.AboutNodeName not in [n.Name for n in self.ShNodes]:
raise ValueError(
f"dc {dc.Name} AboutNodeName {dc.AboutNodeName} not in ShNodes!"
)
captured_by_node = next(
(n for n in self.ShNodes if n.Name == dc.CapturedByNodeName), None
)
if not captured_by_node:
raise ValueError(
f"dc {dc.Name} CapturedByNodeName {dc.CapturedByNodeName} not in ShNodes!"
)
if captured_by_node.ActorClass == ActorClass.NoActor:
raise ValueError(
f"dc {dc.Name}'s CatpuredByNode cannot have ActorClass NoActor!"
)
return self

@model_validator(mode="after")
def check_axiom_2(self) -> Self:
"""
Node Handle Hierarchy Consistency. Every ShNode with a handle containing at least
two words (separated by '.') has an immediate boss: another ShNode whose handle
matches the original handle minus its last word.
"""
existing_handles = {get_handle(node) for node in self.ShNodes}
for node in self.ShNodes:
handle = get_handle(node)
if "." in handle:
boss_handle = ".".join(handle.split(".")[:-1])
if boss_handle not in existing_handles:
raise ValueError(
f"node {node.Name} with handle {handle} missing"
" its immediate boss!"
)
return self


def get_handle(node: SpaceheatNodeGt) -> str:
if node.Handle:
return node.Handle
return node.Name
9 changes: 6 additions & 3 deletions src/gwproto/named_types/scada_params.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Type scada.params, version 000"""
"""Type scada.params, version 001"""

from typing import Literal
from typing import Literal, Optional

from pydantic import BaseModel, ConfigDict

from gwproto.named_types.ha1_params import Ha1Params
from gwproto.property_format import (
LeftRightDotStr,
SpaceheatName,
Expand All @@ -18,7 +19,9 @@ class ScadaParams(BaseModel):
ToName: SpaceheatName
UnixTimeMs: UTCMilliseconds
MessageId: UUID4Str
NewParams: Optional[Ha1Params] = None
OldParams: Optional[Ha1Params] = None
TypeName: Literal["scada.params"] = "scada.params"
Version: Literal["000"] = "000"
Version: Literal["001"] = "001"

model_config = ConfigDict(extra="allow")
23 changes: 23 additions & 0 deletions tests/named_types/test_ha1_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Tests ha1.params type, version 000"""

from gwproto.named_types import Ha1Params


def test_ha1_params_generated() -> None:
d = {
"AlphaTimes10": 120,
"BetaTimes100": -22,
"GammaEx6": 0,
"IntermediatePowerKw": 1.5,
"IntermediateRswtF": 100,
"DdPowerKw": 12,
"DdRswtF": 160,
"DdDeltaTF": 20,
"HpMaxKwTh": 6,
"TypeName": "ha1.params",
"Version": "000",
}

d2 = Ha1Params.model_validate(d).model_dump(exclude_none=True)

assert d2 == d
46 changes: 37 additions & 9 deletions tests/named_types/test_layout_lite.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Tests layout.lite type, version 000"""

import json
"""Tests layout.lite type, version 001"""

from gwproto.named_types import LayoutLite

Expand All @@ -11,14 +9,44 @@ def test_layout_lite_generated() -> None:
"FromGNodeInstanceId": "98542a17-3180-4f2a-a929-6023f0e7a106",
"MessageCreatedMs": 1728651445746,
"MessageId": "1302c0f8-1983-43b2-90d2-61678d731db3",
"ZoneList": ["Down", "Up"],
"Strategy": "House0",
"ZoneList": ["Down", "Up"],
"TotalStoreTanks": 3,
"ShNodes": [
{
"ActorClass": "Scada",
"DisplayName": "Keene Beech Scada",
"Name": "s",
"ShNodeId": "da9a0427-d6c0-44c0-b51c-492c1e580dc5",
"TypeName": "spaceheat.node.gt",
"Version": "200",
},
{
"ActorClass": "PowerMeter",
"ActorHierarchyName": "s.power-meter",
"ComponentId": "9633adef-2373-422d-8a0e-dfbd16ae081c",
"DisplayName": "Primary Power Meter",
"Name": "power-meter",
"ShNodeId": "6c0563b7-5171-4b1c-bba3-de156bea4b95",
"TypeName": "spaceheat.node.gt",
"Version": "200",
},
{
"ActorClass": "NoActor",
"DisplayName": "Hp Idu",
"InPowerMetering": True,
"Name": "hp-idu",
"NameplatePowerW": 4000,
"ShNodeId": "07b8ca98-12c4-4510-8d0f-14fda2331215",
"TypeName": "spaceheat.node.gt",
"Version": "200",
},
],
"DataChannels": [
{
"Name": "hp-idu-pwr",
"DisplayName": "Hp IDU",
"AboutNodeName": "hp-idu-pwr",
"AboutNodeName": "hp-idu",
"CapturedByNodeName": "power-meter",
"TelemetryName": "PowerW",
"TerminalAssetAlias": "hw1.isone.me.versant.keene.beech.ta",
Expand Down Expand Up @@ -120,9 +148,9 @@ def test_layout_lite_generated() -> None:
"PicoKOhms": 30,
"Samples": 1000,
"SendMicroVolts": True,
"AsyncCaptureDeltaMicroVolts": 2000,
"TempCalcMethod": "SimpleBetaForPico",
"ThermistorBeta": 3977,
"AsyncCaptureDeltaMicroVolts": 2000,
"TypeName": "pico.tank.module.component.gt",
"Version": "000",
}
Expand Down Expand Up @@ -173,9 +201,9 @@ def test_layout_lite_generated() -> None:
}
],
"TypeName": "layout.lite",
"Version": "000",
"Version": "001",
}

t2 = LayoutLite.model_validate(d).model_dump_json(exclude_none=True)
d2 = json.loads(t2)
d2 = LayoutLite.model_validate(d).model_dump(exclude_none=True)

assert d2 == d
17 changes: 15 additions & 2 deletions tests/named_types/test_scada_params.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests scada.params type, version 000"""
"""Tests scada.params type, version 001"""

from gwproto.named_types import ScadaParams

Expand All @@ -10,8 +10,21 @@ def test_scada_params_generated() -> None:
"ToName": "h",
"UnixTimeMs": 1731637846788,
"MessageId": "37b64437-f5b2-4a80-b5fc-3d5a9f6b5b59",
"NewParams": {
"AlphaTimes10": 120,
"BetaTimes100": -22,
"GammaEx6": 0,
"IntermediatePowerKw": 1.5,
"IntermediateRswtF": 100,
"DdPowerKw": 12,
"DdRswtF": 160,
"DdDeltaTF": 20,
"HpMaxKwTh": 6,
"TypeName": "ha1.params",
"Version": "000",
},
"TypeName": "scada.params",
"Version": "000",
"Version": "001",
}

d2 = ScadaParams.model_validate(d).model_dump(exclude_none=True)
Expand Down

0 comments on commit a660509

Please sign in to comment.