From bd311edccda1ae11f6c258ab57ad27d505047280 Mon Sep 17 00:00:00 2001 From: Manuel Fritsch Date: Wed, 10 Feb 2021 07:38:55 +0100 Subject: [PATCH] "mash profiles added" --- cbpi/__init__.py | 2 +- cbpi/api/__init__.py | 3 +- cbpi/api/base.py | 64 +++++++++ cbpi/api/extension.py | 2 + cbpi/api/sensor.py | 17 +-- cbpi/api/step.py | 115 +++++++--------- cbpi/api/timer.py | 59 +++++++++ cbpi/controller/actor_controller.py | 2 + cbpi/controller/sensor_controller.py | 10 +- cbpi/controller/step_controller.py | 59 +++++---- cbpi/extension/dummysensor/__init__.py | 9 +- cbpi/extension/mashstep/__init__.py | 174 ++++++++++++++++++++++--- config/actor.json | 4 +- config/cbpi_dashboard_1.json | 12 -- config/config.yaml | 2 +- config/kettle.json | 2 +- config/sensor.json | 9 ++ config/step_data.json | 26 +++- sample.py | 8 ++ 19 files changed, 422 insertions(+), 157 deletions(-) create mode 100644 cbpi/api/base.py create mode 100644 cbpi/api/timer.py create mode 100644 sample.py diff --git a/cbpi/__init__.py b/cbpi/__init__.py index a131110..6468dfe 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.0.17" \ No newline at end of file +__version__ = "4.0.0.18" \ No newline at end of file diff --git a/cbpi/api/__init__.py b/cbpi/api/__init__.py index c265b9a..d66629b 100644 --- a/cbpi/api/__init__.py +++ b/cbpi/api/__init__.py @@ -14,7 +14,8 @@ __all__ = ["CBPiActor", "SensorException", "ActorException", "CBPiSensor", - "CBPiStep"] + "CBPiStep", + "Stop_Reason"] from cbpi.api.actor import * from cbpi.api.sensor import * diff --git a/cbpi/api/base.py b/cbpi/api/base.py new file mode 100644 index 0000000..47830a7 --- /dev/null +++ b/cbpi/api/base.py @@ -0,0 +1,64 @@ +from abc import abstractmethod, ABCMeta +import asyncio +from cbpi.api.config import ConfigType +import time + +import logging + + +class CBPiBase(metaclass=ABCMeta): + + def get_static_config_value(self,name,default): + return self.cbpi.static_config.get(name, default) + + def get_config_value(self,name,default): + return self.cbpi.config.get(name, default=default) + + async def set_config_value(self,name,value): + return await self.cbpi.config.set(name,value) + + async def add_config_value(self, name, value, type: ConfigType, description, options=None): + await self.cbpi.add(name, value, type, description, options=None) + + def get_kettle(self,id): + return self.cbpi.kettle.find_by_id(id) + + async def set_target_temp(self,id, temp): + await self.cbpi.kettle.set_target_temp(id, temp) + + def get_sensor(self,id): + return self.cbpi.sensor.find_by_id(id) + + def get_sensor_value(self,id): + return self.cbpi.sensor.get_sensor_value(id) + + def get_actor(self,id): + return self.cbpi.actor.find_by_id(id) + + def get_actor_state(self,id): + try: + actor = self.cbpi.actor.find_by_id(id) + return actor.get("instance").get_state() + except: + logging.error("Faild to read actor state in step - actor {}".format(id)) + return None + + + + async def actor_on(self,id): + + try: + print("\n\n ON\n\n\n\n" ) + await self.cbpi.actor.on(id) + except Exception as e: + pass + + async def actor_off(self,id): + try: + print("\n\n OFF\n\n\n\n" ) + await self.cbpi.actor.off(id) + except Exception as e: + print("E", e) + pass + + diff --git a/cbpi/api/extension.py b/cbpi/api/extension.py index e5f00c2..b3a0148 100644 --- a/cbpi/api/extension.py +++ b/cbpi/api/extension.py @@ -50,3 +50,5 @@ class CBPiExtension(): except: logger.warning("Faild to load config %s/config.yaml" % path) + + diff --git a/cbpi/api/sensor.py b/cbpi/api/sensor.py index c91f48a..604c9d1 100644 --- a/cbpi/api/sensor.py +++ b/cbpi/api/sensor.py @@ -3,9 +3,9 @@ from abc import abstractmethod, ABCMeta from cbpi.api.extension import CBPiExtension from cbpi.api.config import ConfigType +from cbpi.api.base import CBPiBase - -class CBPiSensor(metaclass=ABCMeta): +class CBPiSensor(CBPiBase, metaclass=ABCMeta): def __init__(self, cbpi, id, props): self.cbpi = cbpi @@ -35,18 +35,6 @@ class CBPiSensor(metaclass=ABCMeta): def get_unit(self): pass - def get_static_config_value(self,name,default): - return self.cbpi.static_config.get(name, default) - - def get_config_value(self,name,default): - return self.cbpi.config.get(name, default=default) - - async def set_config_value(self,name,value): - return await self.cbpi.config.set(name,value) - - async def add_config_value(self, name, value, type: ConfigType, description, options=None): - await self.cbpi.add(name, value, type, description, options=None) - def push_update(self, value): try: self.cbpi.ws.send(dict(topic="sensorstate", id=self.id, value=value)) @@ -57,5 +45,4 @@ class CBPiSensor(metaclass=ABCMeta): self.running = True async def stop(self): - self.running = False \ No newline at end of file diff --git a/cbpi/api/step.py b/cbpi/api/step.py index a6943da..823b244 100644 --- a/cbpi/api/step.py +++ b/cbpi/api/step.py @@ -5,8 +5,15 @@ import logging from abc import abstractmethod, ABCMeta import logging from cbpi.api.config import ConfigType +from cbpi.api.base import CBPiBase +from enum import Enum +__all__ = ["Stop_Reason", "CBPiStep"] +class Stop_Reason(Enum): + STOP = 1 + NEXT = 2 -class CBPiStep(metaclass=ABCMeta): + +class CBPiStep(CBPiBase, metaclass=ABCMeta): def __init__(self, cbpi, id, name, props) : self.cbpi = cbpi self.props = {**props} @@ -17,91 +24,61 @@ class CBPiStep(metaclass=ABCMeta): self.stop_reason = None self.pause = False self.task = None + self._task = None self._exception_count = 0 self._max_exceptions = 2 - self.state_msg = "No state" + self.state_msg = "" def get_state(self): return self.state_msg - def stop(self): - self.stop_reason = "STOP" - self.running = False - - def start(self): - self.running = True + def push_update(self): + self.cbpi.step.push_udpate() + + async def stop(self): + self.stop_reason = Stop_Reason.STOP + self._task.cancel() + await self._task + + async def start(self): self.stop_reason = None - - def next(self): - self.stop_reason = "NEXT" - self.running = False + self._task = asyncio.create_task(self.run()) + self._task.add_done_callback(self.cbpi.step.done) + + async def next(self): + self.stop_reason = Stop_Reason.NEXT + self._task.cancel() async def reset(self): pass + + + + def on_props_update(self, props): + self.props = props async def update(self, props): await self.cbpi.step.update_props(self.id, props) async def run(self): - while self.running: - try: - await self.execute() - except Exception as e: - self._exception_count += 1 - logging.error("Step has thrown exception") - if self._exception_count >= self._max_exceptions: - self.stop_reason = "MAX_EXCEPTIONS" - return (self.id, self.stop_reason) - await asyncio.sleep(1) + try: + while True: + try: + await self.execute() + except asyncio.CancelledError as e: + raise e + except Exception as e: + self._exception_count += 1 + logging.error("Step has thrown exception") + if self._exception_count >= self._max_exceptions: + self.stop_reason = "MAX_EXCEPTIONS" + return (self.id, self.stop_reason) + except asyncio.CancelledError as e: + return self.id, self.stop_reason + - return (self.id, self.stop_reason) - - @abstractmethod async def execute(self): pass + - def get_static_config_value(self,name,default): - return self.cbpi.static_config.get(name, default) - - def get_config_value(self,name,default): - return self.cbpi.config.get(name, default=default) - - async def set_config_value(self,name,value): - return await self.cbpi.config.set(name,value) - - async def add_config_value(self, name, value, type: ConfigType, description, options=None): - await self.cbpi.add(name, value, type, description, options=None) - - def get_kettle(self,id): - return self.cbpi.kettle.find_by_id(id) - - async def set_target_temp(self,id, temp): - await self.cbpi.kettle.set_target_temp(id, temp) - - def get_sensor(self,id): - return self.cbpi.sensor.find_by_id(id) - - def get_actor(self,id): - return self.cbpi.actor.find_by_id(id) - - def get_actor_state(self,id): - try: - actor = self.cbpi.actor.find_by_id(id) - return actor.get("instance").get_state() - except: - logging.error("Faild to read actor state in step - actor {}".format(id)) - return None - - async def actor_on(self,id): - - try: - await self.cbpi.actor.on(id) - except: - pass - - async def actor_off(self,id): - try: - await self.cbpi.actor.off(id) - except: - pass \ No newline at end of file diff --git a/cbpi/api/timer.py b/cbpi/api/timer.py new file mode 100644 index 0000000..9929a61 --- /dev/null +++ b/cbpi/api/timer.py @@ -0,0 +1,59 @@ +import time +import asyncio +import math + + +class Timer(object): + + def __init__(self, timeout, callback, update = None) -> None: + super().__init__() + self.timeout = timeout + self._timemout = self.timeout + self._task = None + self._callback = callback + self._update = update + self.start_time = None + + async def _job(self): + self.start_time = time.time() + self.count = int(round(self._timemout, 0)) + try: + for seconds in range(self.count, -1, -1): + if self._update is not None: + await self._update(seconds, self.format_time(seconds)) + await asyncio.sleep(1) + self._callback() + except asyncio.CancelledError: + end = time.time() + duration = end - self.start_time + self._timemout = self._timemout - duration + + def start(self): + self._task = asyncio.create_task(self._job()) + + async def stop(self): + self._task.cancel() + await self._task + + def reset(self): + if self.is_running is True: + return + self._timemout = self.timeout + + def is_running(self): + return not self._task.done() + + def set_time(self,timeout): + if self.is_running is True: + return + self.timeout = timeout + + def get_time(self): + return self.format_time(int(round(self._timemout,0))) + + def format_time(self, time): + pattern = '{0:02d}:{1:02d}:{2:02d}' + seconds = time % 60 + minutes = math.floor(time / 60) % 60 + hours = math.floor(time / 3600) + return pattern.format(hours, minutes, seconds) diff --git a/cbpi/controller/actor_controller.py b/cbpi/controller/actor_controller.py index 1f2b3eb..f5752f4 100644 --- a/cbpi/controller/actor_controller.py +++ b/cbpi/controller/actor_controller.py @@ -12,6 +12,7 @@ class ActorController(BasicController): item = self.find_by_id(id) instance = item.get("instance") await instance.on() + await self.push_udpate() except Exception as e: logging.error("Faild to switch on Actor {} {}".format(id, e)) @@ -20,6 +21,7 @@ class ActorController(BasicController): item = self.find_by_id(id) instance = item.get("instance") await instance.off() + await self.push_udpate() except Exception as e: logging.error("Faild to switch on Actor {} {}".format(id, e)) diff --git a/cbpi/controller/sensor_controller.py b/cbpi/controller/sensor_controller.py index 4afb4a9..65bcbac 100644 --- a/cbpi/controller/sensor_controller.py +++ b/cbpi/controller/sensor_controller.py @@ -9,10 +9,16 @@ class SensorController(BasicController): def create_dict(self, data): try: instance = data.get("instance") - state = state=instance.get_state() + state =instance.get_state() except Exception as e: logging.error("Faild to create sensor dict {} ".format(e)) state = dict() return dict(name=data.get("name"), id=data.get("id"), type=data.get("type"), state=state,props=data.get("props", [])) - \ No newline at end of file + + def get_sensor_value(self, id): + try: + return self.find_by_id(id).get("instance").get_state() + except Exception as e: + logging.error("Faild read sensor value {} {} ".format(id, e)) + return None \ No newline at end of file diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index 8edf9c6..0da53bc 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -7,7 +7,7 @@ import shortuuid import logging import os.path -from ..api.step import CBPiStep +from ..api.step import CBPiStep, Stop_Reason @@ -59,8 +59,15 @@ class StepController: async def update(self, id, data): logging.info("update step") - - self.profile = list(map(lambda old: {**old, **data} if old["id"] == id else old, self.profile)) + def merge_data(id, old, data): + step = {**old, **data} + try: + step["instance"] = self.create_step(id,data["type"], data["name"], data["props"]) + except Exception as e: + logging.error("Faild create step instance during update props") + return step + + self.profile = list(map(lambda old: {**merge_data(id, old, data)} if old["id"] == id else old, self.profile)) await self.save() return self.find_by_id(id) @@ -69,7 +76,7 @@ class StepController: data = dict(basic=self.basic_data, profile=list(map(lambda x: dict(name=x["name"], type=x.get("type"), id=x["id"], status=x["status"],props=x["props"]), self.profile))) with open(self.path, "w") as file: json.dump(data, file, indent=4, sort_keys=True) - await self.push_udpate() + self.push_udpate() async def start(self): # already running @@ -88,7 +95,7 @@ class StepController: step = self.find_by_status("I") if step is not None: - logging.info("Start Step") + logging.info("####### Start Step") await self.start_step(step) await self.save() @@ -102,9 +109,14 @@ class StepController: if step is not None: instance = step.get("instance") if instance is not None: - logging.info("Next") - instance.next() - await instance.task + await instance.next() + step = self.find_by_status("P") + if step is not None: + instance = step.get("instance") + if instance is not None: + step["status"] = "D" + await self.save() + await self.start() else: logging.info("No Step is running") @@ -123,9 +135,7 @@ class StepController: if step != None and step.get("instance") is not None: logging.info("CALLING STOP STEP") instance = step.get("instance") - instance.stop() - # wait for task to be finished - await instance.task + await instance.stop() logging.info("STEP STOPPED") step["status"] = "P" await self.save() @@ -139,10 +149,10 @@ class StepController: logging.info("Reset %s" % item.get("name")) item["status"] = "I" await item["instance"].reset() - await self.push_udpate() + self.push_udpate() def create_step(self, id, type, name, props): - + print(id, type, name, props) try: type_cfg = self.types.get(type) clazz = type_cfg.get("class") @@ -169,7 +179,7 @@ class StepController: return self.profile[index], self.profile[index+direction] = self.profile[index+direction], self.profile[index] await self.save() - await self.push_udpate() + self.push_udpate() async def delete(self, id): step = self.find_by_id(id) @@ -188,22 +198,23 @@ class StepController: # Stopping all running task if instance.task != None and instance.task.done() is False: logging.info("Stop Step") - instance.stop() + await instance.stop() await instance.task await self.save() def done(self, task): + id, reason = task.result() + print("DONE", id, reason) if reason == "MAX_EXCEPTIONS": step_current = self.find_by_id(id) step_current["status"] = "E" self._loop.create_task(self.save()) return - if reason == "NEXT": + if reason == Stop_Reason.NEXT: step_current = self.find_by_status("A") if step_current is not None: - step_current["status"] = "D" async def wrapper(): ## TODO DONT CALL SAVE @@ -221,25 +232,27 @@ class StepController: def get_index_by_id(self, id): return next((i for i, item in enumerate(self.profile) if item["id"] == id), None) - async def push_udpate(self): + def push_udpate(self): self.cbpi.ws.send(dict(topic="step_update", data=list(map(lambda x: self.create_dict(x), self.profile)))) async def start_step(self,step): logging.info("Start Step") - step.get("instance").start() - step["instance"].task = self._loop.create_task(step["instance"].run()) - step["instance"].task .add_done_callback(self.done) + try: + await step["instance"].start() + except Exception as e: + print(".........",e) step["status"] = "A" + print("STARTED",step) async def update_props(self, id, props): logging.info("SAVE PROPS") step = self.find_by_id(id) step["props"] = props await self.save() - await self.push_udpate() + self.push_udpate() async def save_basic(self, data): logging.info("SAVE Basic Data") self.basic_data = {**self.basic_data, **data,} await self.save() - await self.push_udpate() + self.push_udpate() diff --git a/cbpi/extension/dummysensor/__init__.py b/cbpi/extension/dummysensor/__init__.py index 3d4556a..32a6bf2 100644 --- a/cbpi/extension/dummysensor/__init__.py +++ b/cbpi/extension/dummysensor/__init__.py @@ -28,13 +28,10 @@ class CustomSensor(CBPiSensor): print("ACTION!", kwargs) async def run(self): - + while self.running is True: - print(self.get_config_value("TEMP_UNIT", "NONE")) - print(self.get_static_config_value("port", "NONE")) - await self.set_config_value("BREWERY_NAME", "WOOHOO HELLO") - - self.value = random.randint(0,50) + + self.value = random.randint(0,10) self.push_update(self.value) await asyncio.sleep(1) diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index ec748ee..dea23fd 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -1,31 +1,168 @@ + import asyncio -import time -import random +from cbpi.api.timer import Timer + from cbpi.api import * +import logging -@parameters([Property.Number(label="Timer", configurable=True), +@parameters([Property.Number(label="Timer", description="Time in Minutes", configurable=True), Property.Number(label="Temp", configurable=True), + Property.Sensor(label="Sensor"), Property.Kettle(label="Kettle")]) class MashStep(CBPiStep): + def __init__(self, cbpi, id, name, props): + super().__init__(cbpi, id, name, props) + self.timer = None + + def timer_done(self): + self.state_msg = "Done" + asyncio.create_task(self.next()) + + async def timer_update(self, seconds, time): + self.state_msg = "{}".format(time) + self.push_update() + + def start_timer(self): + if self.timer is None: + self.time = int(self.props.get("Timer", 0)) + self.timer = Timer(self.time, self.timer_done, self.timer_update) + self.timer.start() + + async def stop_timer(self): + if self.timer is not None: + await self.timer.stop() + self.state_msg = "{}".format(self.timer.get_time()) + + async def next(self): + if self.timer is not None: + await self.timer.stop() + self.state_msg = "" + await super().next() + + async def stop(self): + await super().stop() + await self.stop_timer() + + async def reset(self): + self.state_msg = "" + self.timer = None + await super().reset() async def execute(self): - try: - kid = self.props.get("Kettle", None) - kettle = self.get_kettle(kid) - actor = self.get_actor(kettle.get("heater")) - print(self.get_actor_state(kettle.get("heater"))) - await self.cbpi.kettle.set_target_temp(kid, random.randint(0,50)) - if self.v is True: - await self.actor_on(kettle.get("heater")) - else: - await self.actor_off(kettle.get("heater")) - self.v = not self.v - except: - pass + if self.timer is None: + self.state_msg = "Waiting for Target Temp" + self.push_update() + else: + if self.timer is not None and self.timer.is_running() is False: + self.start_timer() + sensor_value = 0 - + while True: + await asyncio.sleep(1) + sensor_value = self.get_sensor_value(self.props.get("Sensor")) + if sensor_value.get("value") >= 2 and self.timer == None: + self.start_timer() + +@parameters([Property.Number(label="Timer", description="Time in Minutes", configurable=True)]) +class WaitStep(CBPiStep): + + def __init__(self, cbpi, id, name, props): + super().__init__(cbpi, id, name, props) + self.timer = None + + def timer_done(self): + self.state_msg = "Done" + + asyncio.create_task(self.next()) + + async def timer_update(self, seconds, time): + self.state_msg = "{}".format(time) + self.push_update() + + def start_timer(self): + if self.timer is None: + self.time = int(self.props.get("Timer", 0)) + self.timer = Timer(self.time, self.timer_done, self.timer_update) + self.timer.start() + + async def stop_timer(self): + if self.timer is not None: + await self.timer.stop() + self.state_msg = "{}".format(self.timer.get_time()) + + async def next(self): + if self.timer is not None: + await self.timer.stop() + self.state_msg = "" + await super().next() + + async def stop(self): + await super().stop() + await self.stop_timer() + + async def reset(self): + self.state_msg = "" + self.timer = None + await super().reset() + + async def execute(self): + self.start_timer() + while True: + await asyncio.sleep(1) + +@parameters([Property.Number(label="Timer", description="Time in Seconds", configurable=True), + Property.Actor(label="Actor")]) +class ActorStep(CBPiStep): + + def __init__(self, cbpi, id, name, props): + super().__init__(cbpi, id, name, props) + self.timer = None + + def timer_done(self): + self.state_msg = "Done" + asyncio.create_task(self.actor_off(self.actor_id)) + asyncio.create_task(self.next()) + + async def timer_update(self, seconds, time): + self.state_msg = "{}".format(time) + self.push_update() + + def start_timer(self): + if self.timer is None: + self.time = int(self.props.get("Timer", 0)) + self.timer = Timer(self.time, self.timer_done, self.timer_update) + self.timer.start() + + async def stop_timer(self): + if self.timer is not None: + await self.timer.stop() + self.state_msg = "{}".format(self.timer.get_time()) + + async def next(self): + if self.timer is not None: + await self.timer.stop() + self.state_msg = "" + await super().next() + + async def stop(self): + await super().stop() + await self.actor_off(self.actor_id) + await self.stop_timer() + + async def reset(self): + self.state_msg = "" + self.timer = None + await super().reset() + + async def execute(self): + self.start_timer() + self.actor_id = self.props.get("Actor") + await self.actor_on(self.actor_id) + while True: + await asyncio.sleep(1) + def setup(cbpi): ''' This method is called by the server during startup @@ -34,5 +171,8 @@ def setup(cbpi): :param cbpi: the cbpi core :return: ''' + + cbpi.plugin.register("ActorStep", ActorStep) + cbpi.plugin.register("WaitStep", WaitStep) cbpi.plugin.register("MashStep", MashStep) diff --git a/config/actor.json b/config/actor.json index 7e3bd1d..1ff2d62 100644 --- a/config/actor.json +++ b/config/actor.json @@ -2,7 +2,7 @@ "data": [ { "id": "YwGzXvWMpmbLb6XobesL8n", - "name": "111", + "name": "Actor 1", "props": { "GPIO": 4, "Inverted": "Yes" @@ -12,7 +12,7 @@ }, { "id": "EsmZwWi9Qp3bzmXqq7N3Ly", - "name": "HALO", + "name": "Actor 2", "props": { "Frequency": "20", "GPIO": 5 diff --git a/config/cbpi_dashboard_1.json b/config/cbpi_dashboard_1.json index eabd696..6f54656 100644 --- a/config/cbpi_dashboard_1.json +++ b/config/cbpi_dashboard_1.json @@ -102,18 +102,6 @@ 365, 160 ], - [ - 285, - 175 - ], - [ - 430, - 420 - ], - [ - 240, - 350 - ], [ 220, 160 diff --git a/config/config.yaml b/config/config.yaml index b5114a8..ba87c9e 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -3,7 +3,7 @@ version: 4.0 index_url: /cbpi_ui/static/index.html plugins: -- cbpi4-ui +- cbpi4ui port: 8080 # login data diff --git a/config/kettle.json b/config/kettle.json index e55c3bd..3ba49a4 100644 --- a/config/kettle.json +++ b/config/kettle.json @@ -8,7 +8,7 @@ "props": {}, "sensor": "8ohkXvFA9UrkHLsxQL38wu", "state": {}, - "target_temp": 25, + "target_temp": 45, "type": "CustomKettleLogic" } ] diff --git a/config/sensor.json b/config/sensor.json index f51d600..cc6bb2d 100644 --- a/config/sensor.json +++ b/config/sensor.json @@ -11,6 +11,15 @@ "value": 0 }, "type": "OneWire" + }, + { + "id": "JUGteK9KrSVPDxboWjBS4N", + "name": "Test2", + "props": {}, + "state": { + "value": 0 + }, + "type": "CustomSensor" } ] } \ No newline at end of file diff --git a/config/step_data.json b/config/step_data.json index 6c26721..bd1b5bf 100644 --- a/config/step_data.json +++ b/config/step_data.json @@ -4,15 +4,27 @@ }, "profile": [ { - "id": "Gkjdsu45XPcfJimz4yHc4w", - "name": "Test", + "id": "SeL6hT9WxvA5yTsTakZuu8", + "name": "Pump Left", "props": { - "Kettle": "oHxKz3z5RjbsxfSz6KUgov", - "Temp": "2", - "Timer": "1" + "Actor": "YwGzXvWMpmbLb6XobesL8n", + "Timer": "5" }, - "status": "P", - "type": "MashStep" + "status": "D", + "type": "ActorStep" + }, + { + "id": "YwyRyzA2ePiiXXnET5gEeH", + "name": "Pump Right", + "props": { + "Actor": "EsmZwWi9Qp3bzmXqq7N3Ly", + "Kettle": "oHxKz3z5RjbsxfSz6KUgov", + "Sensor": "JUGteK9KrSVPDxboWjBS4N", + "Temp": "2", + "Timer": "5" + }, + "status": "D", + "type": "ActorStep" } ] } \ No newline at end of file diff --git a/sample.py b/sample.py new file mode 100644 index 0000000..88baf2c --- /dev/null +++ b/sample.py @@ -0,0 +1,8 @@ +import math + + + +timerTime = 3661 + + +print(format_time(timerTime)) \ No newline at end of file