From 06bc69b367ed0f29e0d715f54c0db2779e4918fd Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Tue, 30 Mar 2021 07:46:41 +0200 Subject: [PATCH 1/6] Fixed back hysteresis To addres issue #44 --- cbpi/extension/hysteresis/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpi/extension/hysteresis/__init__.py b/cbpi/extension/hysteresis/__init__.py index 6b77f0f..4f198d2 100644 --- a/cbpi/extension/hysteresis/__init__.py +++ b/cbpi/extension/hysteresis/__init__.py @@ -4,7 +4,7 @@ import logging from cbpi.api import * @parameters([Property.Number(label="OffsetOn", configurable=True, description="Offset below target temp when heater should switched on"), - Property.Number(label="OffsetOff", configurable=True, description="Offset above target temp when heater should switched off")]) + Property.Number(label="OffsetOff", configurable=True, description="Offset below target temp when heater should switched off")]) class Hysteresis(CBPiKettleLogic): async def run(self): @@ -24,7 +24,7 @@ class Hysteresis(CBPiKettleLogic): target_temp = self.get_kettle_target_temp(self.id) if sensor_value < target_temp - self.offset_on: await self.actor_on(self.heater) - elif sensor_value >= target_temp + self.offset_off: + elif sensor_value >= target_temp - self.offset_off: await self.actor_off(self.heater) await asyncio.sleep(1) From 5cb0ce76deba3041d2a0b2327f6926a0bc8fd573 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Tue, 30 Mar 2021 21:00:17 +0200 Subject: [PATCH 2/6] Added Hop alarms to boil step --- cbpi/extension/mashstep/__init__.py | 63 +++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index 4cd76b1..e2f0f40 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -3,6 +3,10 @@ import asyncio from cbpi.api import parameters, Property, action from cbpi.api.step import StepResult, CBPiStep from cbpi.api.timer import Timer +from datetime import datetime +import time +from voluptuous.schema_builder import message +from cbpi.api.dataclasses import NotificationAction, NotificationType @parameters([Property.Number(label="Timer", description="Time in Minutes", configurable=True), @@ -115,20 +119,51 @@ class ActorStep(CBPiStep): return StepResult.DONE -@parameters([Property.Number(label="Timer", description="Time in Minutes", configurable=True), +@parameters([Property.Number(label="Timer", description="Time in Minutes", configurable=True), Property.Number(label="Temp", description="Boil temperature", configurable=True), Property.Sensor(label="Sensor"), - Property.Kettle(label="Kettle")]) + Property.Kettle(label="Kettle"), + Property.Select("First_Wort", options=["Yes","No"], description="First Wort Hop alert if set to Yes"), + Property.Number("Hop_1", configurable = True, description="First Hop alert (minutes before finish)"), + Property.Number("Hop_2", configurable=True, description="Second Hop alert (minutes before finish)"), + Property.Number("Hop_3", configurable=True, description="Third Hop alert (minutes before finish)"), + Property.Number("Hop_4", configurable=True, description="Fourth Hop alert (minutes before finish)"), + Property.Number("Hop_5", configurable=True, description="Fifth Hop alert (minutes before finish)"), + Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)")]) class BoilStep(CBPiStep): + @action("Start Timer", []) + async def start_timer(self): + if self.timer.is_running == None: + self.cbpi.notify(self.name, 'Timer started', NotificationType.INFO) + self.timer.start() + else: + self.cbpi.notify(self.name, 'Timer is already running', NotificationType.WARNING) + + @action("Add 5 Minutes to Timer", []) + async def add_timer(self): + if self.timer.is_running != None: + self.cbpi.notify(self.name, '5 Minutes added', NotificationType.INFO) + await self.timer.add(300) + else: + self.cbpi.notify(self.name, 'Timer must be running to add time', NotificationType.WARNING) + + async def on_timer_done(self, timer): self.summary = "" await self.next() async def on_timer_update(self, timer, seconds): self.summary = Timer.format_time(seconds) + self.remaining_seconds = seconds await self.push_update() + async def check_hop_timer(self, number, value): + if value is not None and value != '' and self.hops_added[number-1] is not True: + if self.remaining_seconds != None and self.remaining_seconds <= (int(value) * 60 + 1): + self.hops_added[number-1]= True + self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO) + async def on_start(self): if self.timer is None: self.timer = Timer(int(self.props.Timer) * 60, on_update=self.on_timer_update, on_done=self.on_timer_done) @@ -136,6 +171,10 @@ class BoilStep(CBPiStep): await self.cbpi.kettle.set_target_temp(self.props.Kettle, int(self.props.Temp)) self.summary = "Waiting for Target Temp" await self.push_update() + self.first_wort_hop_flag = False + self.first_wort_hop=self.props.First_Wort + self.hops_added=["","","","","",""] + self.remaining_seconds = None async def on_stop(self): await self.timer.stop() @@ -145,18 +184,28 @@ class BoilStep(CBPiStep): async def reset(self): self.timer = Timer(int(self.props.Timer) * 60, on_update=self.on_timer_update, on_done=self.on_timer_done) - @action("Start Timer", []) - async def star_timer(self): - - self.timer.start() - async def run(self): + if self.first_wort_hop_flag == False and self.first_wort_hop == "Yes": + self.first_wort_hop_flag = True + self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO) + while self.running == True: await asyncio.sleep(1) sensor_value = self.get_sensor_value(self.props.Sensor) + if sensor_value.get("value") >= int(self.props.Temp) and self.timer.is_running is not True: self.timer.start() + estimated_completion_time = datetime.fromtimestamp(time.time()+ (int(self.props.Timer))*60) + self.cbpi.notify(self.name, 'Timer started. Estimated completion: {}'.format(estimated_completion_time.strftime("%H:%M")), NotificationType.INFO) self.timer.is_running = True + else: + await self.check_hop_timer(1, self.props.Hop_1) + await self.check_hop_timer(2, self.props.Hop_2) + await self.check_hop_timer(3, self.props.Hop_3) + await self.check_hop_timer(4, self.props.Hop_4) + await self.check_hop_timer(5, self.props.Hop_5) + await self.check_hop_timer(6, self.props.Hop_6) + return StepResult.DONE From 28f4113f2fc56c7d19b51900155dc519de28fb5f Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Thu, 8 Apr 2021 14:40:47 +0200 Subject: [PATCH 3/6] Fix to improve accuracy of timer Timer was not accurate as it was depending on the asyncio.sleep function which is most liklely not exactly one second depending on other functions running in parallel. Now the timer is comparable to cbpi3 where it runs until an end time is reached. This is to address issue #66 and has been tested --- cbpi/api/timer.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cbpi/api/timer.py b/cbpi/api/timer.py index 4f20050..a4b697c 100644 --- a/cbpi/api/timer.py +++ b/cbpi/api/timer.py @@ -13,27 +13,29 @@ class Timer(object): self._callback = on_done self._update = on_update self.start_time = None + self.end_time = None def done(self, task): if self._callback is not None: asyncio.create_task(self._callback(self)) async def _job(self): - self.start_time = time.time() - self.count = int(round(self._timemout, 0)) + self.start_time = int(time.time()) + self.end_time = self.start_time + int(round(self._timemout, 0)) + self.count = self.end_time - self.start_time try: while self.count > 0: - self.count -= 1 + self.count = (self.end_time - int(time.time())) if self._update is not None: await self._update(self,self.count) await asyncio.sleep(1) except asyncio.CancelledError: - end = time.time() + end = int(time.time()) duration = end - self.start_time self._timemout = self._timemout - duration async def add(self, seconds): - self.count = self.count + seconds + self.end_time = self.end_time + seconds def start(self): self._task = asyncio.create_task(self._job()) @@ -66,4 +68,4 @@ class Timer(object): seconds = time % 60 minutes = math.floor(time / 60) % 60 hours = math.floor(time / 3600) - return pattern.format(hours, minutes, seconds) \ No newline at end of file + return pattern.format(hours, minutes, seconds) From e89c42f5dd70d2d7d911768ac246ec2b551397de Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Fri, 9 Apr 2021 10:36:06 +0200 Subject: [PATCH 4/6] Added offset and Temp Unit support - Offset can be specified for each newire sensor - Sensor is reporting temp in C or F depending on server settings --- cbpi/extension/onewire/__init__.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cbpi/extension/onewire/__init__.py b/cbpi/extension/onewire/__init__.py index 6851b96..0f57edd 100644 --- a/cbpi/extension/onewire/__init__.py +++ b/cbpi/extension/onewire/__init__.py @@ -51,7 +51,9 @@ class ReadThread (threading.Thread): time.sleep(1) -@parameters([Property.Select(label="Sensor", options=getSensors()), Property.Select(label="Interval", options=[1,5,10,30,60], description="Interval in Seconds")]) +@parameters([Property.Select(label="Sensor", options=getSensors()), + Property.Number(label="offset",configurable = True, default_value = 0, description="Offset for the PT Sensor (Default is 0)"), + Property.Select(label="Interval", options=[1,5,10,30,60], description="Interval in Seconds")]) class OneWire(CBPiSensor): def __init__(self, cbpi, id, props): @@ -62,6 +64,13 @@ class OneWire(CBPiSensor): await super().start() self.name = self.props.get("Sensor") self.interval = self.props.get("Interval", 60) + self.offset = self.props.get("offset") + if self.offset == "" or self.offset is None: + self.offset = 0 + else: + self.offset = float(self.offset) + + print(self.offset) self.t = ReadThread(self.name) self.t.daemon = True def shudown(): @@ -78,7 +87,11 @@ class OneWire(CBPiSensor): async def run(self): while self.running == True: - self.value = self.t.value + self.TEMP_UNIT=self.get_config_value("TEMP_UNIT", "C") + if self.TEMP_UNIT == "C": # Report temp in C if nothing else is selected in settings + self.value = round((self.t.value + self.offset),2) + else: # Report temp in F if unit selected in settings + self.value = round((9.0 / 5.0 * self.t.value + 32 + self.offset), 2) self.log_data(self.value) self.push_update(self.value) From 55953539dad9fb64176fd7b4f38e3e623f3724b1 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 11 Apr 2021 10:44:16 +0200 Subject: [PATCH 5/6] Update dataclasses.py Returns also None when returned string is empty This change is to address issue #68 --- cbpi/api/dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index 52445a0..e35891b 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -30,7 +30,7 @@ class Props: return key in self.__data__ def get(self, key, d=None): - if key in self.__data__: + if key in self.__data__ and self.__data__[key] != "": return self.__data__[key] else: return d From 0828aeacac906491511c09697ce16559bd8f3a7f Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 11 Apr 2021 12:30:34 +0200 Subject: [PATCH 6/6] Simplified offset handling Change is somehow dependent dependent on issue #68 with corresponding change in dataclasses.py. But this is an example on how the change in dataclasses.py would allow to simplify the props handling in case of empty values. --- cbpi/extension/onewire/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cbpi/extension/onewire/__init__.py b/cbpi/extension/onewire/__init__.py index 0f57edd..e1d3641 100644 --- a/cbpi/extension/onewire/__init__.py +++ b/cbpi/extension/onewire/__init__.py @@ -64,13 +64,8 @@ class OneWire(CBPiSensor): await super().start() self.name = self.props.get("Sensor") self.interval = self.props.get("Interval", 60) - self.offset = self.props.get("offset") - if self.offset == "" or self.offset is None: - self.offset = 0 - else: - self.offset = float(self.offset) + self.offset = float(self.props.get("offset",0)) - print(self.offset) self.t = ReadThread(self.name) self.t.daemon = True def shudown():