"mash profiles added"

This commit is contained in:
Manuel Fritsch 2021-02-10 07:38:55 +01:00
parent ab14111787
commit bd311edccd
19 changed files with 422 additions and 157 deletions

View file

@ -1 +1 @@
__version__ = "4.0.0.17" __version__ = "4.0.0.18"

View file

@ -14,7 +14,8 @@ __all__ = ["CBPiActor",
"SensorException", "SensorException",
"ActorException", "ActorException",
"CBPiSensor", "CBPiSensor",
"CBPiStep"] "CBPiStep",
"Stop_Reason"]
from cbpi.api.actor import * from cbpi.api.actor import *
from cbpi.api.sensor import * from cbpi.api.sensor import *

64
cbpi/api/base.py Normal file
View file

@ -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

View file

@ -50,3 +50,5 @@ class CBPiExtension():
except: except:
logger.warning("Faild to load config %s/config.yaml" % path) logger.warning("Faild to load config %s/config.yaml" % path)

View file

@ -3,9 +3,9 @@ from abc import abstractmethod, ABCMeta
from cbpi.api.extension import CBPiExtension from cbpi.api.extension import CBPiExtension
from cbpi.api.config import ConfigType from cbpi.api.config import ConfigType
from cbpi.api.base import CBPiBase
class CBPiSensor(CBPiBase, metaclass=ABCMeta):
class CBPiSensor(metaclass=ABCMeta):
def __init__(self, cbpi, id, props): def __init__(self, cbpi, id, props):
self.cbpi = cbpi self.cbpi = cbpi
@ -35,18 +35,6 @@ class CBPiSensor(metaclass=ABCMeta):
def get_unit(self): def get_unit(self):
pass 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): def push_update(self, value):
try: try:
self.cbpi.ws.send(dict(topic="sensorstate", id=self.id, value=value)) self.cbpi.ws.send(dict(topic="sensorstate", id=self.id, value=value))
@ -57,5 +45,4 @@ class CBPiSensor(metaclass=ABCMeta):
self.running = True self.running = True
async def stop(self): async def stop(self):
self.running = False self.running = False

View file

@ -5,8 +5,15 @@ import logging
from abc import abstractmethod, ABCMeta from abc import abstractmethod, ABCMeta
import logging import logging
from cbpi.api.config import ConfigType 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) : def __init__(self, cbpi, id, name, props) :
self.cbpi = cbpi self.cbpi = cbpi
self.props = {**props} self.props = {**props}
@ -17,91 +24,61 @@ class CBPiStep(metaclass=ABCMeta):
self.stop_reason = None self.stop_reason = None
self.pause = False self.pause = False
self.task = None self.task = None
self._task = None
self._exception_count = 0 self._exception_count = 0
self._max_exceptions = 2 self._max_exceptions = 2
self.state_msg = "No state" self.state_msg = ""
def get_state(self): def get_state(self):
return self.state_msg return self.state_msg
def stop(self): def push_update(self):
self.stop_reason = "STOP" self.cbpi.step.push_udpate()
self.running = False
def start(self): async def stop(self):
self.running = True self.stop_reason = Stop_Reason.STOP
self._task.cancel()
await self._task
async def start(self):
self.stop_reason = None self.stop_reason = None
self._task = asyncio.create_task(self.run())
self._task.add_done_callback(self.cbpi.step.done)
def next(self): async def next(self):
self.stop_reason = "NEXT" self.stop_reason = Stop_Reason.NEXT
self.running = False self._task.cancel()
async def reset(self): async def reset(self):
pass pass
def on_props_update(self, props):
self.props = props
async def update(self, props): async def update(self, props):
await self.cbpi.step.update_props(self.id, props) await self.cbpi.step.update_props(self.id, props)
async def run(self): async def run(self):
while self.running: try:
try: while True:
await self.execute() try:
except Exception as e: await self.execute()
self._exception_count += 1 except asyncio.CancelledError as e:
logging.error("Step has thrown exception") raise e
if self._exception_count >= self._max_exceptions: except Exception as e:
self.stop_reason = "MAX_EXCEPTIONS" self._exception_count += 1
return (self.id, self.stop_reason) logging.error("Step has thrown exception")
await asyncio.sleep(1) if self._exception_count >= self._max_exceptions:
self.stop_reason = "MAX_EXCEPTIONS"
return (self.id, self.stop_reason) return (self.id, self.stop_reason)
except asyncio.CancelledError as e:
return self.id, self.stop_reason
@abstractmethod @abstractmethod
async def execute(self): async def execute(self):
pass 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

59
cbpi/api/timer.py Normal file
View file

@ -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)

View file

@ -12,6 +12,7 @@ class ActorController(BasicController):
item = self.find_by_id(id) item = self.find_by_id(id)
instance = item.get("instance") instance = item.get("instance")
await instance.on() await instance.on()
await self.push_udpate()
except Exception as e: except Exception as e:
logging.error("Faild to switch on Actor {} {}".format(id, e)) logging.error("Faild to switch on Actor {} {}".format(id, e))
@ -20,6 +21,7 @@ class ActorController(BasicController):
item = self.find_by_id(id) item = self.find_by_id(id)
instance = item.get("instance") instance = item.get("instance")
await instance.off() await instance.off()
await self.push_udpate()
except Exception as e: except Exception as e:
logging.error("Faild to switch on Actor {} {}".format(id, e)) logging.error("Faild to switch on Actor {} {}".format(id, e))

View file

@ -9,10 +9,16 @@ class SensorController(BasicController):
def create_dict(self, data): def create_dict(self, data):
try: try:
instance = data.get("instance") instance = data.get("instance")
state = state=instance.get_state() state =instance.get_state()
except Exception as e: except Exception as e:
logging.error("Faild to create sensor dict {} ".format(e)) logging.error("Faild to create sensor dict {} ".format(e))
state = dict() state = dict()
return dict(name=data.get("name"), id=data.get("id"), type=data.get("type"), state=state,props=data.get("props", [])) return dict(name=data.get("name"), id=data.get("id"), type=data.get("type"), state=state,props=data.get("props", []))
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

View file

@ -7,7 +7,7 @@ import shortuuid
import logging import logging
import os.path 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): async def update(self, id, data):
logging.info("update step") logging.info("update step")
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: {**old, **data} if old["id"] == id else old, self.profile)) self.profile = list(map(lambda old: {**merge_data(id, old, data)} if old["id"] == id else old, self.profile))
await self.save() await self.save()
return self.find_by_id(id) 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))) 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: with open(self.path, "w") as file:
json.dump(data, file, indent=4, sort_keys=True) json.dump(data, file, indent=4, sort_keys=True)
await self.push_udpate() self.push_udpate()
async def start(self): async def start(self):
# already running # already running
@ -88,7 +95,7 @@ class StepController:
step = self.find_by_status("I") step = self.find_by_status("I")
if step is not None: if step is not None:
logging.info("Start Step") logging.info("####### Start Step")
await self.start_step(step) await self.start_step(step)
await self.save() await self.save()
@ -102,9 +109,14 @@ class StepController:
if step is not None: if step is not None:
instance = step.get("instance") instance = step.get("instance")
if instance is not None: if instance is not None:
logging.info("Next") await instance.next()
instance.next() step = self.find_by_status("P")
await instance.task if step is not None:
instance = step.get("instance")
if instance is not None:
step["status"] = "D"
await self.save()
await self.start()
else: else:
logging.info("No Step is running") logging.info("No Step is running")
@ -123,9 +135,7 @@ class StepController:
if step != None and step.get("instance") is not None: if step != None and step.get("instance") is not None:
logging.info("CALLING STOP STEP") logging.info("CALLING STOP STEP")
instance = step.get("instance") instance = step.get("instance")
instance.stop() await instance.stop()
# wait for task to be finished
await instance.task
logging.info("STEP STOPPED") logging.info("STEP STOPPED")
step["status"] = "P" step["status"] = "P"
await self.save() await self.save()
@ -139,10 +149,10 @@ class StepController:
logging.info("Reset %s" % item.get("name")) logging.info("Reset %s" % item.get("name"))
item["status"] = "I" item["status"] = "I"
await item["instance"].reset() await item["instance"].reset()
await self.push_udpate() self.push_udpate()
def create_step(self, id, type, name, props): def create_step(self, id, type, name, props):
print(id, type, name, props)
try: try:
type_cfg = self.types.get(type) type_cfg = self.types.get(type)
clazz = type_cfg.get("class") clazz = type_cfg.get("class")
@ -169,7 +179,7 @@ class StepController:
return return
self.profile[index], self.profile[index+direction] = self.profile[index+direction], self.profile[index] self.profile[index], self.profile[index+direction] = self.profile[index+direction], self.profile[index]
await self.save() await self.save()
await self.push_udpate() self.push_udpate()
async def delete(self, id): async def delete(self, id):
step = self.find_by_id(id) step = self.find_by_id(id)
@ -188,22 +198,23 @@ class StepController:
# Stopping all running task # Stopping all running task
if instance.task != None and instance.task.done() is False: if instance.task != None and instance.task.done() is False:
logging.info("Stop Step") logging.info("Stop Step")
instance.stop() await instance.stop()
await instance.task await instance.task
await self.save() await self.save()
def done(self, task): def done(self, task):
id, reason = task.result() id, reason = task.result()
print("DONE", id, reason)
if reason == "MAX_EXCEPTIONS": if reason == "MAX_EXCEPTIONS":
step_current = self.find_by_id(id) step_current = self.find_by_id(id)
step_current["status"] = "E" step_current["status"] = "E"
self._loop.create_task(self.save()) self._loop.create_task(self.save())
return return
if reason == "NEXT": if reason == Stop_Reason.NEXT:
step_current = self.find_by_status("A") step_current = self.find_by_status("A")
if step_current is not None: if step_current is not None:
step_current["status"] = "D" step_current["status"] = "D"
async def wrapper(): async def wrapper():
## TODO DONT CALL SAVE ## TODO DONT CALL SAVE
@ -221,25 +232,27 @@ class StepController:
def get_index_by_id(self, id): def get_index_by_id(self, id):
return next((i for i, item in enumerate(self.profile) if item["id"] == id), None) 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)))) 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): async def start_step(self,step):
logging.info("Start Step") logging.info("Start Step")
step.get("instance").start() try:
step["instance"].task = self._loop.create_task(step["instance"].run()) await step["instance"].start()
step["instance"].task .add_done_callback(self.done) except Exception as e:
print(".........",e)
step["status"] = "A" step["status"] = "A"
print("STARTED",step)
async def update_props(self, id, props): async def update_props(self, id, props):
logging.info("SAVE PROPS") logging.info("SAVE PROPS")
step = self.find_by_id(id) step = self.find_by_id(id)
step["props"] = props step["props"] = props
await self.save() await self.save()
await self.push_udpate() self.push_udpate()
async def save_basic(self, data): async def save_basic(self, data):
logging.info("SAVE Basic Data") logging.info("SAVE Basic Data")
self.basic_data = {**self.basic_data, **data,} self.basic_data = {**self.basic_data, **data,}
await self.save() await self.save()
await self.push_udpate() self.push_udpate()

View file

@ -30,11 +30,8 @@ class CustomSensor(CBPiSensor):
async def run(self): async def run(self):
while self.running is True: 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) self.push_update(self.value)
await asyncio.sleep(1) await asyncio.sleep(1)

View file

@ -1,30 +1,167 @@
import asyncio import asyncio
import time from cbpi.api.timer import Timer
import random
from cbpi.api import * 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.Number(label="Temp", configurable=True),
Property.Sensor(label="Sensor"),
Property.Kettle(label="Kettle")]) Property.Kettle(label="Kettle")])
class MashStep(CBPiStep): 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): async def execute(self):
try: if self.timer is None:
kid = self.props.get("Kettle", None) self.state_msg = "Waiting for Target Temp"
kettle = self.get_kettle(kid) self.push_update()
actor = self.get_actor(kettle.get("heater")) else:
print(self.get_actor_state(kettle.get("heater"))) if self.timer is not None and self.timer.is_running() is False:
await self.cbpi.kettle.set_target_temp(kid, random.randint(0,50)) self.start_timer()
if self.v is True: sensor_value = 0
await self.actor_on(kettle.get("heater"))
else:
await self.actor_off(kettle.get("heater"))
self.v = not self.v
except:
pass
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): def setup(cbpi):
''' '''
@ -34,5 +171,8 @@ def setup(cbpi):
:param cbpi: the cbpi core :param cbpi: the cbpi core
:return: :return:
''' '''
cbpi.plugin.register("ActorStep", ActorStep)
cbpi.plugin.register("WaitStep", WaitStep)
cbpi.plugin.register("MashStep", MashStep) cbpi.plugin.register("MashStep", MashStep)

View file

@ -2,7 +2,7 @@
"data": [ "data": [
{ {
"id": "YwGzXvWMpmbLb6XobesL8n", "id": "YwGzXvWMpmbLb6XobesL8n",
"name": "111", "name": "Actor 1",
"props": { "props": {
"GPIO": 4, "GPIO": 4,
"Inverted": "Yes" "Inverted": "Yes"
@ -12,7 +12,7 @@
}, },
{ {
"id": "EsmZwWi9Qp3bzmXqq7N3Ly", "id": "EsmZwWi9Qp3bzmXqq7N3Ly",
"name": "HALO", "name": "Actor 2",
"props": { "props": {
"Frequency": "20", "Frequency": "20",
"GPIO": 5 "GPIO": 5

View file

@ -102,18 +102,6 @@
365, 365,
160 160
], ],
[
285,
175
],
[
430,
420
],
[
240,
350
],
[ [
220, 220,
160 160

View file

@ -3,7 +3,7 @@ version: 4.0
index_url: /cbpi_ui/static/index.html index_url: /cbpi_ui/static/index.html
plugins: plugins:
- cbpi4-ui - cbpi4ui
port: 8080 port: 8080
# login data # login data

View file

@ -8,7 +8,7 @@
"props": {}, "props": {},
"sensor": "8ohkXvFA9UrkHLsxQL38wu", "sensor": "8ohkXvFA9UrkHLsxQL38wu",
"state": {}, "state": {},
"target_temp": 25, "target_temp": 45,
"type": "CustomKettleLogic" "type": "CustomKettleLogic"
} }
] ]

View file

@ -11,6 +11,15 @@
"value": 0 "value": 0
}, },
"type": "OneWire" "type": "OneWire"
},
{
"id": "JUGteK9KrSVPDxboWjBS4N",
"name": "Test2",
"props": {},
"state": {
"value": 0
},
"type": "CustomSensor"
} }
] ]
} }

View file

@ -4,15 +4,27 @@
}, },
"profile": [ "profile": [
{ {
"id": "Gkjdsu45XPcfJimz4yHc4w", "id": "SeL6hT9WxvA5yTsTakZuu8",
"name": "Test", "name": "Pump Left",
"props": { "props": {
"Kettle": "oHxKz3z5RjbsxfSz6KUgov", "Actor": "YwGzXvWMpmbLb6XobesL8n",
"Temp": "2", "Timer": "5"
"Timer": "1"
}, },
"status": "P", "status": "D",
"type": "MashStep" "type": "ActorStep"
},
{
"id": "YwyRyzA2ePiiXXnET5gEeH",
"name": "Pump Right",
"props": {
"Actor": "EsmZwWi9Qp3bzmXqq7N3Ly",
"Kettle": "oHxKz3z5RjbsxfSz6KUgov",
"Sensor": "JUGteK9KrSVPDxboWjBS4N",
"Temp": "2",
"Timer": "5"
},
"status": "D",
"type": "ActorStep"
} }
] ]
} }

8
sample.py Normal file
View file

@ -0,0 +1,8 @@
import math
timerTime = 3661
print(format_time(timerTime))