Skip to content

Commit

Permalink
Merge pull request #304 from thegridelectric/td/scadablindweather
Browse files Browse the repository at this point in the history
Td/scadablindweather
  • Loading branch information
jessicamillar authored Jan 9, 2025
2 parents f16f1c3 + 7081229 commit 319aae1
Show file tree
Hide file tree
Showing 18 changed files with 789 additions and 579 deletions.
29 changes: 26 additions & 3 deletions gw_spaceheat/actors/api_tank_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from pydantic import BaseModel
from result import Ok, Result
from actors.scada_actor import ScadaActor
from named_types import PicoMissing
from named_types import PicoMissing, ChannelFlatlined

R_FIXED_KOHMS = 5.65 # The voltage divider resistors in the TankModule
THERMISTOR_T0 = 298 # i.e. 25 degrees
Expand Down Expand Up @@ -79,6 +79,13 @@ def __init__(
self.last_heard_a = time.time()
self.last_heard_b = time.time()
self.last_error_report = time.time()
try:
self.depth1_channel = self.layout.data_channels[f"{self.name}-depth1"]
self.depth2_channel = self.layout.data_channels[f"{self.name}-depth2"]
self.depth3_channel = self.layout.data_channels[f"{self.name}-depth3"]
self.depth4_channel = self.layout.data_channels[f"{self.name}-depth4"]
except KeyError as e:
raise Exception(f"Problem setting up ApiTankModule channels! {e}")

@cached_property
def microvolts_path(self) -> str:
Expand Down Expand Up @@ -279,7 +286,7 @@ def flatline_seconds(self) -> int:
for cfg in self._component.gt.ConfigList
if cfg.ChannelName == f"{self.name}-depth1"
)
return cfg.CapturePeriodS * 2.1
return cfg.CapturePeriodS

@property
def monitored_names(self) -> Sequence[MonitoredName]:
Expand All @@ -301,16 +308,32 @@ async def main(self):
self.pico_cycler,
PicoMissing(ActorName=self.name, PicoHwUid=self.pico_a_uid),
)
self._send_to(
self.synth_generator,
ChannelFlatlined(FromName=self.name, Channel=self.depth1_channel)
)
self._send_to(
self.synth_generator,
ChannelFlatlined(FromName=self.name, Channel=self.depth2_channel)
)
# self._send_to(self.primary_scada, Problems(warnings=[f"{self.pico_a_uid} down"]).problem_event(summary=self.name))
self.last_error_report = time.time()
if self.b_missing():
self._send_to(
self.pico_cycler,
PicoMissing(ActorName=self.name, PicoHwUid=self.pico_b_uid),
)
self._send_to(
self.synth_generator,
ChannelFlatlined(FromName=self.name, Channel=self.depth3_channel)
)
self._send_to(
self.synth_generator,
ChannelFlatlined(FromName=self.name, Channel=self.depth4_channel)
)
# self._send_to(self.primary_scada, Problems(warnings=[f"{self.pico_b_uid} down"]).problem_event(summary=self.name))
self.last_error_report = time.time()
await asyncio.sleep(self.flatline_seconds())
await asyncio.sleep(10)

def simple_beta_for_pico(self, volts: float, fahrenheit=False) -> float:
"""
Expand Down
15 changes: 15 additions & 0 deletions gw_spaceheat/actors/atn.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,21 @@ def set_load_overestimation_percent(self, load_overestimation_percent: float) ->
except Exception as e:
self.logger.error(f"Failed to set LoadOverestimationPercent! {e}")

def set_load_overestimation_percent(self, load_overestimation_percent: float) -> None:
if load_overestimation_percent < 0 or load_overestimation_percent > 100:
self.log("Invalid entry, load_overestimation_percent should be a value between 0 and 100")
return
if self.ha1_params is None:
self.send_layout()
else:
try:
new = Ha1Params.model_validate(
{**self.ha1_params.model_dump(), "LoadOverestimationPercent": load_overestimation_percent}
)
self.send_new_params(new)
except Exception as e:
self.logger.error(f"Failed to set LoadOverestimationPercent! {e}")

def set_dist_010(self, val: int = 30) -> None:
self.send_threadsafe(
Message(
Expand Down
66 changes: 31 additions & 35 deletions gw_spaceheat/actors/atomic_ally.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
from gwproto.enums import FsmReportType
from gwproto.named_types import (Alert, FsmAtomicReport, FsmFullReport,
MachineStates)
from named_types import EnergyInstruction, GoDormant, Ha1Params, WakeUp
from named_types import EnergyInstruction, GoDormant, Ha1Params, WakeUp, HeatingForecast
from result import Ok, Result
from transitions import Machine

from actors.scada_actor import ScadaActor
from actors.scada_data import ScadaData
from actors.synth_generator import WeatherForecast
from named_types import RemainingElec, ScadaInit


Expand Down Expand Up @@ -125,7 +124,7 @@ def __init__(self, name: str, services: ServicesInterface):
self.is_simulated = self.settings.is_simulated
self.log(f"Params: {self.params}")
self.log(f"self.is_simulated: {self.is_simulated}")
self.weather = None
self.forecasts: HeatingForecast = None
self.remaining_elec_wh = None

@property
Expand Down Expand Up @@ -153,7 +152,6 @@ def process_message(self, message: Message) -> Result[bool, BaseException]:
self.log(f"Received an EnergyInstruction for {message.Payload.AvgPowerWatts} Watts average power")
self.remaining_elec_wh = message.Payload.AvgPowerWatts
self.check_and_update_state()

case GoDormant():
if self.state != AtomicAllyState.Dormant.value:
# GoDormant: AnyOther -> Dormant ...
Expand All @@ -173,16 +171,9 @@ def process_message(self, message: Message) -> Result[bool, BaseException]:
# WakeUp: Dormant -> WaitingNoElec ... will turn off heat pmp
self.trigger_event(AtomicAllyEvent.WakeUp)
self.check_and_update_state()
case WeatherForecast():
self.log("Received weather forecast")
self.weather = {
'time': message.Payload.Time,
'oat': message.Payload.OatForecast,
'ws': message.Payload.WsForecast,
'required_swt': message.Payload.RswtForecast,
'avg_power': message.Payload.AvgPowerForecast,
'required_swt_deltaT': message.Payload.RswtDeltaTForecast,
}
case HeatingForecast():
self.log("Received forecast")
self.forecasts: HeatingForecast = message.Payload

return Ok(True)

Expand Down Expand Up @@ -231,8 +222,8 @@ def monitored_names(self) -> Sequence[MonitoredName]:

def check_and_update_state(self) -> None:
self.log(f"State: {self.state}")
if not self.weather:
self.log("Strange ... Do not have weather yet! Not updating state since can't check buffer state")
if not self.forecasts:
self.log("Strange ... Do not have forecasts yet! Not updating state since can't check buffer state")
return
if self.state != AtomicAllyState.Dormant:
previous_state = self.state
Expand Down Expand Up @@ -316,12 +307,12 @@ def check_and_update_state(self) -> None:
async def main(self):
await asyncio.sleep(2)
self._send_to(self.primary_scada, ScadaInit(FromGNodeAlias=self.layout.atn_g_node_alias))
# SynthGenerator gets weather ASAP on boot, including various fallbacks
# SynthGenerator gets forecasts ASAP on boot, including various fallbacks
# if the request does not work. So wait a bit if
if self.weather is None:
if self.forecasts is None:
await asyncio.sleep(5)
if self.weather is None:
raise Exception("No access to weather! Won't be able to heat the buffer")
if self.forecasts is None:
raise Exception("No access to forecasts! Won't be able to heat the buffer")

while not self._stop_requested:
self._send(PatInternalWatchdogMessage(src=self.name))
Expand Down Expand Up @@ -414,42 +405,47 @@ def no_more_elec(self) -> bool:
self.log(f"Electricity available: {self.remaining_elec_wh} Wh")
return False

def is_buffer_empty(self) -> bool:
def is_buffer_empty(self, really_empty=False) -> bool:
if H0CN.buffer.depth2 in self.latest_temperatures:
buffer_empty_ch = H0CN.buffer.depth2
if really_empty:
buffer_empty_ch = H0CN.buffer.depth1
else:
buffer_empty_ch = H0CN.buffer.depth2
elif H0CN.dist_swt in self.latest_temperatures:
buffer_empty_ch = H0CN.dist_swt
else:
self.alert(alias="buffer_empty_fail", msg="Impossible to know if the buffer is empty!")
return False
max_rswt_next_3hours = max(self.weather['required_swt'][:3])
max_deltaT_rswt_next_3_hours = max(self.weather['required_swt_deltaT'][:3])
max_rswt_next_3hours = max(self.forecasts.RswtF[:3])
max_deltaT_rswt_next_3_hours = max(self.forecasts.RswtDeltaTF[:3])
min_buffer = round(max_rswt_next_3hours - max_deltaT_rswt_next_3_hours,1)
if self.latest_temperatures[buffer_empty_ch] < min_buffer:
self.log(f"Buffer empty ({buffer_empty_ch}: {round(self.latest_temperatures[buffer_empty_ch],1)} < {min_buffer} F)")
buffer_empty_ch_temp = round(self.to_fahrenheit(self.latest_temperatures[buffer_empty_ch]/1000),1)
if buffer_empty_ch_temp < min_buffer:
self.log(f"Buffer empty ({buffer_empty_ch}: {buffer_empty_ch_temp} < {min_buffer} F)")
return True
else:
self.log(f"Buffer not empty ({buffer_empty_ch}: {round(self.latest_temperatures[buffer_empty_ch],1)} >= {min_buffer} F)")
self.log(f"Buffer not empty ({buffer_empty_ch}: {buffer_empty_ch_temp} >= {min_buffer} F)")
return False

def is_buffer_full(self) -> bool:
if H0CN.buffer.depth4 in self.latest_temperatures:
buffer_full_temp = H0CN.buffer.depth4
buffer_full_ch = H0CN.buffer.depth4
elif H0CN.buffer_cold_pipe in self.latest_temperatures:
buffer_full_temp = H0CN.buffer_cold_pipe
buffer_full_ch = H0CN.buffer_cold_pipe
elif "StoreDischarge" in self.state and H0CN.store_cold_pipe in self.latest_temperatures:
buffer_full_temp = H0CN.store_cold_pipe
buffer_full_ch = H0CN.store_cold_pipe
elif 'hp-ewt' in self.latest_temperatures:
buffer_full_temp = 'hp-ewt'
buffer_full_ch = 'hp-ewt'
else:
self.alert(alias="buffer_full_fail", msg="Impossible to know if the buffer is full!")
return False
max_buffer = round(max(self.weather['required_swt'][:3]),1)
if self.latest_temperatures[buffer_full_temp] > max_buffer:
self.log(f"Buffer full (layer 4: {round(self.latest_temperatures[buffer_full_temp],1)} > {max_buffer} F)")
max_buffer = round(max(self.forecasts.RswtF[:3]),1)
buffer_full_ch_temp = round(self.to_fahrenheit(self.latest_temperatures[buffer_full_ch]/1000),1)
if buffer_full_ch_temp > max_buffer:
self.log(f"Buffer full ({buffer_full_ch}: {buffer_full_ch_temp} > {max_buffer} F)")
return True
else:
self.log(f"Buffer not full (layer 4: {round(self.latest_temperatures[buffer_full_temp],1)} <= {max_buffer} F)")
self.log(f"Buffer not full ({buffer_full_ch}: {buffer_full_ch_temp} <= {max_buffer} F)")
return False

def is_storage_full(self) -> bool:
Expand Down
Loading

0 comments on commit 319aae1

Please sign in to comment.