diff --git a/components/sud.py b/components/sud.py index 5b657f6..13d3fd7 100644 --- a/components/sud.py +++ b/components/sud.py @@ -48,17 +48,12 @@ def _build_step(number, defaults, raw_step): class Sud(AttributeChange): def __init__(self, path): AttributeChange.__init__(self) + self.path = path with open(path) as f: data = json.load(f) - self.name = data.get('Name', '') - self.description = data.get('Description', '') - - step_defaults = data.get('default', {}).get('step', {}) - self.schedule = [_build_step(i + 1, step_defaults, raw) for i, raw in enumerate(data['steps'])] - - self.pot_mass = data.get('pot_mass', 0) - self.pot_material = data.get('pot_material') + (self.name, self.description, self.schedule, + self.pot_mass, self.pot_material) = self._parse_data(data) self.index = -1 self.hold_remaining = 0.0 @@ -66,6 +61,52 @@ class Sud(AttributeChange): self.step = None self.user_message = None + @staticmethod + def _parse_data(data): + """Parses a sud.json document into the (name, description, schedule, + pot_mass, pot_material) tuple Sud needs. Computed up front rather + than assigned straight onto self, so a malformed upload() can't + leave a half-applied schedule in place.""" + name = data.get('Name', '') + description = data.get('Description', '') + + step_defaults = data.get('default', {}).get('step', {}) + schedule = [_build_step(i + 1, step_defaults, raw) for i, raw in enumerate(data['steps'])] + + pot_mass = data.get('pot_mass', 0) + pot_material = data.get('pot_material') + + return name, description, schedule, pot_mass, pot_material + + def download(self): + """Returns the sud.json document currently on disk, for a client to + edit and hand back to upload().""" + with open(self.path) as f: + return json.load(f) + + def upload(self, data): + """Replaces the schedule with a new sud.json document and persists + it to disk, resetting to IDLE as if freshly loaded from that file. + Refused while a brew is in progress, or if data is malformed - in + either case the current schedule is left untouched.""" + if self.state not in (SudState.IDLE, SudState.DONE): + return False + try: + parsed = self._parse_data(data) + except (KeyError, TypeError): + return False + + self.name, self.description, self.schedule, self.pot_mass, self.pot_material = parsed + with open(self.path, 'w') as f: + json.dump(data, f, indent='\t') + + self.index = -1 + self.hold_remaining = 0.0 + self.state = SudState.IDLE + self.step = None + self.user_message = None + return True + def derive_plant_params(self, grain_mass, water_mass): """Lumped (mass, specific heat) lifted from pot_mass/pot_material and the current step's grain_mass/water_mass, for use as Pot's "M"/"C" diff --git a/scripts/demos/sud/demo_sud_upload_download.py b/scripts/demos/sud/demo_sud_upload_download.py new file mode 100644 index 0000000..088fffa --- /dev/null +++ b/scripts/demos/sud/demo_sud_upload_download.py @@ -0,0 +1,45 @@ +import json +import shutil +import tempfile +from pathlib import Path + +from components.sud import Sud, SudState + +# Console-only walkthrough of Sud.download()/upload() (no plot - there's no +# numeric data here, just JSON plumbing). Runs against a scratch copy of a +# real sud.json so sude/sud_0010.json itself is never touched. + +if __name__ == '__main__': + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "sud.json" + shutil.copy("sude/sud_0010.json", path) + + sud = Sud(str(path)) + print(f"Loaded '{sud.name}', {len(sud.schedule)} steps") + + doc = sud.download() + assert doc['Name'] == sud.name + print("download() returned the on-disk document") + + sud.state = SudState.RAMPING + assert sud.upload(doc) is False + print("upload() refused while RAMPING") + + sud.state = SudState.IDLE + doc['Name'] = 'Sud-Renamed' + doc['steps'][0]['descr'] = 'Edited step' + assert sud.upload(doc) is True + assert sud.name == 'Sud-Renamed' + assert sud.schedule[0]['descr'] == 'Edited step' + assert sud.state == SudState.IDLE + print("upload() applied the edit and reset to IDLE") + + on_disk = json.loads(path.read_text()) + assert on_disk['Name'] == 'Sud-Renamed' + print("upload() persisted the edit to disk") + + assert sud.upload({'Name': 'broken'}) is False + assert sud.name == 'Sud-Renamed' + print("upload() rejected malformed data, left schedule untouched") + + print("All sud upload/download checks passed") diff --git a/tasks/sud.py b/tasks/sud.py index ab9f4bf..4ff1de1 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -69,6 +69,11 @@ class SudTask(ATask): self.sud.start() elif 'Confirm' in pair[0]: self.sud.confirm() + elif 'Download' in pair[0]: + await self.send({'Json': self.sud.download()}) + elif 'Upload' in pair[0]: + if self.sud.upload(pair[1]): + await self.send({'Name': self.sud.name, 'Description': self.sud.description}) async def send(self, data): await self.msg_handler.send(data)