diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3731cc6..976fe56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,13 +9,16 @@ on: pull_request: env: - image-name: ghcr.io/${{ github.repository_owner }}/craftbeerpi4 + IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/craftbeerpi4 jobs: build: runs-on: ubuntu-latest + permissions: + packages: write name: Builds the source distribution package steps: + - name: Checkout source uses: actions/checkout@v2 @@ -54,8 +57,11 @@ jobs: id: prep run: | + IMAGE_NAME_LOWERCASE=${IMAGE_NAME,,} + echo "Using image name $IMAGE_NAME_LOWERCASE" + PUBLISH_IMAGE=false - TAGS="${{ env.image-name }}:dev" + TAGS="$IMAGE_NAME_LOWERCASE:dev" # Define the image that will be used as a cached image # to speed up the build process @@ -65,18 +71,21 @@ jobs: # when building master/main use :latest tag and the version number # from the cbpi/__init__.py file VERSION=$(grep -o -E "(([0-9]{1,2}[.]?){2,3}[0-9]+)" cbpi/__init__.py) - LATEST_IMAGE=${{ env.image-name }}:latest + LATEST_IMAGE=$IMAGE_NAME_LOWERCASE:latest BUILD_CACHE_IMAGE_NAME=${LATEST_IMAGE} - TAGS="${LATEST_IMAGE},${{ env.image-name }}:v${VERSION}" - PUBLISH_IMAGE=true + TAGS="${LATEST_IMAGE},$IMAGE_NAME_LOWERCASE:v${VERSION}" + PUBLISH_IMAGE="true" elif [[ $GITHUB_REF_NAME == development ]]; then - PUBLISH_IMAGE=true + PUBLISH_IMAGE="true" fi - # Set output parameters. - echo ::set-output name=tags::${TAGS} - echo ::set-output name=publish_image::${PUBLISH_IMAGE} - echo ::set-output name=build_cache_image_name::${BUILD_CACHE_IMAGE_NAME} + + echo "tags: $TAGS" + echo "publish_image: $PUBLISH_IMAGE" + echo "cache_name: $BUILD_CACHE_IMAGE_NAME" + echo "tags=$TAGS" >> $GITHUB_OUTPUT + echo "publish_image=$PUBLISH_IMAGE" >> $GITHUB_OUTPUT + echo "cache_name=$BUILD_CACHE_IMAGE_NAME" >> $GITHUB_OUTPUT - name: Set up QEMU uses: docker/setup-qemu-action@master @@ -88,23 +97,23 @@ jobs: uses: docker/setup-buildx-action@master - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: builder: ${{ steps.buildx.outputs.name }} context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 target: deploy - push: ${{ steps.prep.outputs.publish_image }} + push: ${{ steps.prep.outputs.publish_image == 'true' }} tags: ${{ steps.prep.outputs.tags }} - cache-from: type=registry,ref=${{ steps.prep.outputs.build_cache_image_name }} + cache-from: type=registry,ref=${{ steps.prep.outputs.cache_name }} cache-to: type=inline labels: | org.opencontainers.image.title=${{ github.event.repository.name }} diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 17b857f..3e9f879 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.1.5" +__version__ = "4.1.6" __codename__ = "Groundhog Day" diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index 486382a..5007a64 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -64,6 +64,10 @@ class Actor: def to_dict(self): return dict(id=self.id, name=self.name, type=self.type, props=self.props.to_dict(), state=self.instance.get_state(), power=self.power) +class DataType(Enum): + VALUE="value" + DATETIME="datetime" + STRING="string" @dataclass class Sensor: @@ -73,6 +77,7 @@ class Sensor: state: bool = False type: str = None instance: str = None + datatype: DataType = DataType.VALUE def __str__(self): return "name={} props={}, state={}".format(self.name, self.props, self.state) diff --git a/cbpi/api/sensor.py b/cbpi/api/sensor.py index a539aca..9874ee8 100644 --- a/cbpi/api/sensor.py +++ b/cbpi/api/sensor.py @@ -2,6 +2,7 @@ import asyncio import logging from abc import abstractmethod, ABCMeta from cbpi.api.extension import CBPiExtension +from cbpi.api.dataclasses import DataType from cbpi.api.base import CBPiBase @@ -16,6 +17,7 @@ class CBPiSensor(CBPiBase, metaclass=ABCMeta): self.data_logger = None self.state = False self.running = False + self.datatype=DataType.VALUE def init(self): pass @@ -33,13 +35,14 @@ class CBPiSensor(CBPiBase, metaclass=ABCMeta): pass def push_update(self, value, mqtt = True): + 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, datatype=self.datatype.value)) if mqtt: - self.cbpi.push_update("cbpi/sensordata/{}".format(self.id), dict(id=self.id, value=value), retain=True) + self.cbpi.push_update("cbpi/sensordata/{}".format(self.id), dict(id=self.id, value=value, datatype=self.datatype.value), retain=True) # self.cbpi.push_update("cbpi/sensor/{}/udpate".format(self.id), dict(id=self.id, value=value), retain=True) except: - logging.error("Failed to push sensor update") + logging.error("Failed to push sensor update for sensor {}".format(self.id)) async def start(self): pass diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index 9e8c9a0..1a93fb1 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -158,11 +158,14 @@ class LogController: dateparse = lambda dates: [datetime.datetime.strptime(d, '%Y-%m-%d %H:%M:%S') for d in dates] result = dict() for id in ids: - all_filenames = glob.glob(os.path.join(self.logsFolderPath,f"sensor_{id}.log*")) - df = pd.concat([pd.read_csv(f, parse_dates=['DateTime'], date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames]) - df = df.resample('60s').max() - df = df.dropna() - result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()} + try: + all_filenames = glob.glob(os.path.join(self.logsFolderPath,f"sensor_{id}.log*")) + df = pd.concat([pd.read_csv(f, parse_dates=['DateTime'], date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames]) + df = df.resample('60s').max() + df = df.dropna() + result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()} + except: + pass return result diff --git a/cbpi/extension/dummysensor/__init__.py b/cbpi/extension/dummysensor/__init__.py index c935df2..8e1bbc9 100644 --- a/cbpi/extension/dummysensor/__init__.py +++ b/cbpi/extension/dummysensor/__init__.py @@ -4,7 +4,8 @@ import random import logging from cbpi.api import * from cbpi.api.base import CBPiBase -from cbpi.api.dataclasses import Kettle, Props, Fermenter +from cbpi.api.dataclasses import Kettle, Props, Fermenter, DataType +import time @parameters([]) class CustomSensor(CBPiSensor): diff --git a/cbpi/extension/timer/__init__.py b/cbpi/extension/timer/__init__.py new file mode 100644 index 0000000..ed16e34 --- /dev/null +++ b/cbpi/extension/timer/__init__.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from aiohttp import web +import logging +import asyncio +from cbpi.api import * +from cbpi.api import base +from time import strftime, gmtime +from cbpi.api.timer import Timer +from cbpi.api.dataclasses import DataType +from cbpi.api.dataclasses import NotificationAction, NotificationType + +logger = logging.getLogger(__name__) +@parameters([]) +class AlarmTimer(CBPiSensor): + + def __init__(self, cbpi, id, props): + super(AlarmTimer, self).__init__(cbpi, id, props) + self.value = "00:00:00" + self.datatype=DataType.STRING + self.timer = None + self.time=0 + self.stopped=False + self.sensor=self.get_sensor(self.id) + + @action(key="Set Timer", parameters=[Property.Number(label="time", description="Time in Minutes", configurable=True)]) + async def set(self, time = 0,**kwargs): + self.stopped=False + self.time = float(time) + self.value=self.calculate_time(self.time) + if self.timer is not None: + await self.timer.stop() + self.timer = Timer(int(self.time * 60), on_update=self.on_timer_update, on_done=self.on_timer_done) + await self.timer.stop() + self.timer.is_running = False + logging.info("Set Timer") + + @action(key="Start Timer", parameters=[]) + async def start(self , **kwargs): + if self.timer is None: + self.timer = Timer(int(self.time * 60), on_update=self.on_timer_update, on_done=self.on_timer_done) + + if self.timer.is_running is not True: + self.timer.start() + self.stopped=False + self.timer.is_running = True + else: + self.cbpi.notify(self.sensor.name,'Timer is already running', NotificationType.WARNING) + + @action(key="Stop Timer", parameters=[]) + async def stop(self , **kwargs): + self.stopped=False + await self.timer.stop() + self.timer.is_running = False + logging.info("Stop Timer") + + @action(key="Reset Timer", parameters=[]) + async def Reset(self , **kwargs): + self.stopped=False + if self.timer is not None: + await self.timer.stop() + self.value=self.calculate_time(self.time) + self.timer = Timer(int(self.time * 60), on_update=self.on_timer_update, on_done=self.on_timer_done) + await self.timer.stop() + self.timer.is_running = False + logging.info("Reset Timer") + + async def on_timer_done(self, timer): + #self.value = "Stopped" + if self.stopped is True: + self.cbpi.notify(self.sensor.name,'Timer done', NotificationType.SUCCESS) + + self.timer.is_running = False + pass + + async def on_timer_update(self, timer, seconds): + self.stopped=True + self.value = Timer.format_time(seconds) + + async def run(self): + while self.running is True: + self.push_update(self.value) + await asyncio.sleep(1) + pass + + def get_state(self): + return dict(value=self.value) + + def calculate_time(self, time): + return strftime("%H:%M:%S", gmtime(time*60)) + +def setup(cbpi): + cbpi.plugin.register("AlarmTimer", AlarmTimer) + pass \ No newline at end of file diff --git a/cbpi/extension/timer/config.yaml b/cbpi/extension/timer/config.yaml new file mode 100644 index 0000000..a777645 --- /dev/null +++ b/cbpi/extension/timer/config.yaml @@ -0,0 +1,3 @@ +name: timer +version: 4 +active: true \ No newline at end of file diff --git a/cbpi/http_endpoints/http_log.py b/cbpi/http_endpoints/http_log.py index 8183edb..44cac80 100644 --- a/cbpi/http_endpoints/http_log.py +++ b/cbpi/http_endpoints/http_log.py @@ -4,6 +4,7 @@ from cbpi.utils.utils import json_dumps from cbpi.api import request_mapping import os import json +import logging class LogHttpEndpoints: def __init__(self,cbpi): @@ -189,7 +190,8 @@ class LogHttpEndpoints: description: successful operation. """ data = await request.json() - return web.json_response(await self.cbpi.log.get_data2(data), dumps=json_dumps) + values = await self.cbpi.log.get_data2(data) + return web.json_response(values, dumps=json_dumps) @request_mapping(path="/{name}", method="DELETE", auth_required=False) diff --git a/tests/cbpi-test-config/config.json b/tests/cbpi-test-config/config.json index d6ab711..8fef776 100644 --- a/tests/cbpi-test-config/config.json +++ b/tests/cbpi-test-config/config.json @@ -45,6 +45,22 @@ "type": "string", "value": "Some New Brewery Name" }, + "BoilAutoTimer": { + "description": "Start Boil timer automatically if Temp does not change for 5 Minutes and is above 95C/203F", + "name": "BoilAutoTimer", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "select", + "value": "No" + }, "BoilKettle": { "description": "Define Kettle that is used for Boil, Whirlpool and Cooldown. If not selected, MASH_TUN will be used", "name": "BoilKettle", @@ -107,6 +123,13 @@ "type": "select", "value": "No" }, + "INFLUXDBMEASUREMENT": { + "description": "Name of the measurement in your INFLUXDB database (default: measurement)", + "name": "INFLUXDBMEASUREMENT", + "options": null, + "type": "string", + "value": "measurement" + }, "INFLUXDBNAME": { "description": "Name of your influxdb database name (If INFLUXDBCLOUD set to Yes use bucket of your influxdb cloud database)", "name": "INFLUXDBNAME",