Merge pull request #143 from PiBrewing/development

Development
This commit is contained in:
Alexander Vollkopf 2024-07-13 12:31:24 +02:00 committed by GitHub
commit 8b42600781
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 296 additions and 50 deletions

View file

@ -1,3 +1,3 @@
__version__ = "4.4.1.rc1" __version__ = "4.4.3"
__codename__ = "Yeast Starter" __codename__ = "Yeast Starter"

View file

@ -8,6 +8,11 @@ from cbpi.utils.utils import load_config
from zipfile import ZipFile from zipfile import ZipFile
from cbpi.craftbeerpi import CraftBeerPi from cbpi.craftbeerpi import CraftBeerPi
import os import os
try:
import pwd
module_pwd=True
except:
module_pwd=False
import pkgutil import pkgutil
import shutil import shutil
import click import click
@ -165,7 +170,8 @@ class CraftBeerPiCli():
else: else:
print("CraftBeerPi Autostart is {}OFF{}".format(Fore.RED,Style.RESET_ALL)) print("CraftBeerPi Autostart is {}OFF{}".format(Fore.RED,Style.RESET_ALL))
elif(name == "on"): elif(name == "on"):
user=os.getlogin() #user=os.getlogin()
user=pwd.getpwuid(os.getuid()).pw_name
path="/usr/local/bin/cbpi" path="/usr/local/bin/cbpi"
if os.path.exists("/home/"+user+"/.local/bin/cbpi") is True: if os.path.exists("/home/"+user+"/.local/bin/cbpi") is True:
path="/home/"+user+"/.local/bin/cbpi" path="/home/"+user+"/.local/bin/cbpi"

View file

@ -11,6 +11,7 @@ mqtt_host: localhost
mqtt_port: 1883 mqtt_port: 1883
mqtt_username: "" mqtt_username: ""
mqtt_password: "" mqtt_password: ""
mqtt_offset: false
username: cbpi username: cbpi
password: 123 password: 123

View file

@ -17,13 +17,12 @@ import os.path
from os import listdir from os import listdir
from os.path import isfile, join from os.path import isfile, join
import json import json
import shortuuid import math
import yaml import yaml
from ..api.step import StepMove, StepResult, StepState from ..api.step import StepMove, StepResult, StepState
import re import re
import base64 import base64
class UploadController: class UploadController:
def __init__(self, cbpi): def __init__(self, cbpi):
@ -70,9 +69,12 @@ class UploadController:
return [] return []
async def get_brewfather_recipes(self,offset=0): async def get_brewfather_recipes(self,offset=0):
limit = 50
length = self.cbpi.config.get('brewfather_list_length',50)
repeat = True
brewfather = True brewfather = True
result=[] result=[]
self.url="https://api.brewfather.app/v1/recipes" self.url="https://api.brewfather.app/v2/recipes"
brewfather_user_id = self.cbpi.config.get("brewfather_user_id", None) brewfather_user_id = self.cbpi.config.get("brewfather_user_id", None)
if brewfather_user_id == "" or brewfather_user_id is None: if brewfather_user_id == "" or brewfather_user_id is None:
brewfather = False brewfather = False
@ -84,25 +86,63 @@ class UploadController:
if brewfather == True: if brewfather == True:
encodedData = base64.b64encode(bytes(f"{brewfather_user_id}:{brewfather_api_key}", "ISO-8859-1")).decode("ascii") encodedData = base64.b64encode(bytes(f"{brewfather_user_id}:{brewfather_api_key}", "ISO-8859-1")).decode("ascii")
headers={"Authorization": "Basic %s" % encodedData} headers={"Authorization": "Basic %s" % encodedData}
parameters={"limit": 50, 'offset': offset} parameters={"limit": limit}
async with aiohttp.ClientSession(headers=headers) as bf_session: while repeat == True:
async with bf_session.get(self.url, params=parameters) as r: try:
bf_recipe_list = await r.json() async with aiohttp.ClientSession(headers=headers) as bf_session:
await bf_session.close() async with bf_session.get(self.url, params=parameters) as r:
if r.status == 429:
if bf_recipe_list: try:
for row in bf_recipe_list: seconds=int(r.headers['Retry-After'])
recipe_id = row['_id'] minutes=round(seconds/60)
name = row['name'] except:
element = {'value': recipe_id, 'label': name} seconds=None
result.append(element) if not seconds:
return result logging.error("Too many requests to BF api. Try again later")
else: self.cbpi.notify("Error", "Too many requests to BF api. Try again later", NotificationType.ERROR)
return [] else:
logging.error(f"Too many requests to BF api. Try in {minutes} minutes again.")
else: self.cbpi.notify("Error", f"Too many requests to BF api. Try in {minutes} minutes again.", NotificationType.ERROR)
return [] repeat = False
logging.error(r.headers['Retry-After'])
else:
bf_recipe_list = await r.json()
await bf_session.close()
except Exception as e:
logging.error(e)
repeat = False
try:
if bf_recipe_list:
for row in bf_recipe_list:
recipe_id = row['_id']
name = row['name']
element = {'value': recipe_id, 'label': name}
result.append(element)
else:
repeat = False
except Exception as e:
logging.error(e)
try:
if len(bf_recipe_list) != limit:
repeat = False
else:
parameters={"limit": limit, 'start_after': recipe_id}
except Exception as e:
logging.error(e)
try:
newlist = sorted(result, key=lambda d: d['label'])
listlength=len(newlist)
max=math.floor(listlength/length)
sortlist=[]
for i in range(0 , max+1):
sortlist.append({ 'value': i*length, 'label': i*length })
return newlist, sortlist, length
except:
logging.error("Return empty BF recipe list")
sortlist=[{ 'value': 0, 'label': '0' }]
return result, sortlist, length
def get_creation_path(self): def get_creation_path(self):
creation_path = self.cbpi.config.get("RECIPE_CREATION_PATH", "upload") creation_path = self.cbpi.config.get("RECIPE_CREATION_PATH", "upload")
@ -738,7 +778,7 @@ class UploadController:
brewfather = True brewfather = True
result=[] result=[]
self.bf_url="https://api.brewfather.app/v1/recipes/" + Recipe_ID self.bf_url="https://api.brewfather.app/v2/recipes/" + Recipe_ID
brewfather_user_id = self.cbpi.config.get("brewfather_user_id", None) brewfather_user_id = self.cbpi.config.get("brewfather_user_id", None)
if brewfather_user_id == "" or brewfather_user_id is None: if brewfather_user_id == "" or brewfather_user_id is None:
brewfather = False brewfather = False
@ -775,6 +815,21 @@ class UploadController:
except: except:
miscs = None miscs = None
try:
fermentation_steps=bf_recipe['fermentation']['steps']
except:
fermentation_steps=None
if fermentation_steps is not None:
try:
step=fermentation_steps[0]
self.fermentation_step_temp=int(step['stepTemp'])
except:
self.fermentation_step_temp=None
if self.fermentation_step_temp is not None and self.TEMP_UNIT != "C":
self.fermentation_step_temp = round((9.0 / 5.0 * float(self.fermentation_step_temp)+ 32))
FirstWort = self.getFirstWort(hops, "bf") FirstWort = self.getFirstWort(hops, "bf")
await self.create_recipe(RecipeName) await self.create_recipe(RecipeName)
@ -1012,8 +1067,9 @@ class UploadController:
cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None) cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None)
if cooldown_sensor is None or cooldown_sensor == '': if cooldown_sensor is None or cooldown_sensor == '':
cooldown_sensor = self.boilkettle.sensor # fall back to boilkettle sensor if no other sensor is specified cooldown_sensor = self.boilkettle.sensor # fall back to boilkettle sensor if no other sensor is specified
step_timer = "" step_timer = ""
step_temp = int(self.CoolDownTemp)
step_temp = int(self.CoolDownTemp) if (self.fermentation_step_temp is None or self.fermentation_step_temp <= int(self.CoolDownTemp)) else self.fermentation_step_temp
step_string = { "name": "Cooldown", step_string = { "name": "Cooldown",
"props": { "props": {
"Kettle": self.boilid, "Kettle": self.boilid,

View file

@ -8,7 +8,7 @@ import json
from cbpi.api import * from cbpi.api import *
from cbpi.api.config import ConfigType from cbpi.api.config import ConfigType
from cbpi.api.base import CBPiBase from cbpi.api.base import CBPiBase
import glob import glob, yaml
from cbpi import __version__ from cbpi import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -19,6 +19,12 @@ class ConfigUpdate(CBPiExtension):
self.cbpi = cbpi self.cbpi = cbpi
self._task = asyncio.create_task(self.run()) self._task = asyncio.create_task(self.run())
def append_to_yaml(self, file_path, data_to_append):
with open(file_path[0], 'a+') as file:
file.seek(0)
yaml.dump(data_to_append, file, default_flow_style=False)
async def run(self): async def run(self):
logging.info("Check Config for required changes") logging.info("Check Config for required changes")
@ -61,12 +67,13 @@ class ConfigUpdate(CBPiExtension):
AddMashIn = self.cbpi.config.get("AddMashInStep", None) AddMashIn = self.cbpi.config.get("AddMashInStep", None)
bfuserid = self.cbpi.config.get("brewfather_user_id", None) bfuserid = self.cbpi.config.get("brewfather_user_id", None)
bfapikey = self.cbpi.config.get("brewfather_api_key", None) bfapikey = self.cbpi.config.get("brewfather_api_key", None)
bflistlength = self.cbpi.config.get("brewfather_list_length", None)
RecipeCreationPath = self.cbpi.config.get("RECIPE_CREATION_PATH", None) RecipeCreationPath = self.cbpi.config.get("RECIPE_CREATION_PATH", None)
BoilKettle = self.cbpi.config.get("BoilKettle", None) BoilKettle = self.cbpi.config.get("BoilKettle", None)
CONFIG_STATUS = self.cbpi.config.get("CONFIG_STATUS", None) CONFIG_STATUS = self.cbpi.config.get("CONFIG_STATUS", None)
self.version=__version__ self.version=__version__
current_grid = self.cbpi.config.get("current_grid", None) current_grid = self.cbpi.config.get("current_grid", None)
mqtt_offset=self.cbpi.static_config.get("mqtt_offset", None)
if boil_temp is None: if boil_temp is None:
logger.info("INIT Boil Temp Setting") logger.info("INIT Boil Temp Setting")
@ -244,6 +251,21 @@ class ConfigUpdate(CBPiExtension):
await self.cbpi.config.add("brewfather_api_key", "", type=ConfigType.STRING, description="Brewfather API Key", source="craftbeerpi") await self.cbpi.config.add("brewfather_api_key", "", type=ConfigType.STRING, description="Brewfather API Key", source="craftbeerpi")
except: except:
logger.warning('Unable to update config') logger.warning('Unable to update config')
## Check if Brewfather API Key is in config
if bflistlength is None:
logger.info("INIT Brewfather Recipe List Length")
try:
await self.cbpi.config.add("brewfather_list_length", 50, type=ConfigType.SELECT, description="Brewfather Recipe List length",
source="craftbeerpi",
options= [{"label": "5", "value": 5},
{"label": "10", "value": 10},
{"label": "25", "value": 25},
{"label": "50", "value": 50},
{"label": "100", "value": 100}])
except:
logger.warning('Unable to update config')
## Check if Brewfather API Key is in config ## Check if Brewfather API Key is in config
@ -542,6 +564,16 @@ class ConfigUpdate(CBPiExtension):
except Exception as e: except Exception as e:
logging.error(e) logging.error(e)
if mqtt_offset is None:
logging.info("INIT MQTT Offset in static config")
try:
static_config_file=glob.glob(self.cbpi.config_folder.get_file_path('config.yaml'))
data_to_append = {'mqtt_offset': False}
self.append_to_yaml(static_config_file, data_to_append)
pass
except Exception as e:
logging.error(e)
logging.warning('Unable to update database')
## Check if influxdbname is in config ## Check if influxdbname is in config
if CONFIG_STATUS is None or CONFIG_STATUS != self.version: if CONFIG_STATUS is None or CONFIG_STATUS != self.version:

View file

@ -536,7 +536,7 @@ class CooldownStep(CBPiStep):
if time.time() >= self.next_check: if time.time() >= self.next_check:
self.next_check = time.time() + (self.Interval * 60) self.next_check = time.time() + (self.Interval * 60)
cooldown_model = np.poly1d(np.polyfit(self.temp_array, self.time_array, 2)) cooldown_model = np.polynomial.polynomial.Polynomial.fit(self.temp_array, self.time_array, 2)
target_time=cooldown_model(self.target_temp) target_time=cooldown_model(self.target_temp)
target_timestring= datetime.fromtimestamp(target_time) target_timestring= datetime.fromtimestamp(target_time)
self.summary="ECT: {}".format(target_timestring.strftime("%H:%M")) self.summary="ECT: {}".format(target_timestring.strftime("%H:%M"))

View file

@ -8,16 +8,17 @@ import json
import time import time
from datetime import datetime from datetime import datetime
@parameters([Property.Text(label="Topic", configurable=True, description="MQTT Topic"), @parameters([Property.Text(label="Topic", configurable=True, description="MQTT Topic"),
Property.Text(label="PayloadDictionary", configurable=True, default_value="", Property.Text(label="PayloadDictionary", configurable=True, default_value="",
description="Where to find msg in payload, leave blank for raw payload"), description="Where to find msg in payload, leave blank for raw payload"),
Property.Kettle(label="Kettle", description="Reduced logging if Kettle is inactive / range warning in dashboard (only Kettle or Fermenter to be selected)"), Property.Kettle(label="Kettle", description="Reduced logging if Kettle is inactive / range warning in dashboard (only Kettle or Fermenter to be selected)"),
Property.Fermenter(label="Fermenter", description="Reduced logging if Fermenter is inactive / range warning in dashboard (only Kettle or Fermenter to be selected)"), Property.Fermenter(label="Fermenter", description="Reduced logging if Fermenter is inactive / range warning in dashboard (only Kettle or Fermenter to be selected)"),
Property.Number(label="ReducedLogging", configurable=True, description="Reduced logging frequency in seconds if selected Kettle or Fermenter is inactive (default:60 sec | 0 disabled)"), Property.Number(label="ReducedLogging", configurable=True, description="Reduced logging frequency in seconds if selected Kettle or Fermenter is inactive (default:60 sec | 0 disabled)"),
Property.Number(label="Timeout", configurable=True, unit="sec", Property.Number(label="Timeout", configurable=True, unit="sec",
description="Timeout in seconds to send notification (default:60 | deactivated: 0)"), description="Timeout in seconds to send notification (default:60 | deactivated: 0)"),
Property.Number(label="TempRange", configurable=True, unit="degree", Property.Number(label="TempRange", configurable=True, unit="degree",
description="Temp range in degree between reading and target temp of fermenter/kettle. Larger difference shows different color in dashboard (default:0 | deactivated: 0)")]) description="Temp range in degree between reading and target temp of fermenter/kettle. Larger difference shows different color in dashboard (default:0 | deactivated: 0)")])
class MQTTSensor(CBPiSensor): class MQTTSensor(CBPiSensor):
def __init__(self, cbpi, id, props): def __init__(self, cbpi, id, props):
@ -135,6 +136,135 @@ class MQTTSensor(CBPiSensor):
async def on_stop(self): async def on_stop(self):
self.subscribed = self.cbpi.satellite.unsubscribe(self.Topic, self.on_message) self.subscribed = self.cbpi.satellite.unsubscribe(self.Topic, self.on_message)
@parameters([Property.Text(label="Topic", configurable=True, description="MQTT Topic"),
Property.Text(label="PayloadDictionary", configurable=True, default_value="",
description="Where to find msg in payload, leave blank for raw payload"),
Property.Kettle(label="Kettle", description="Reduced logging if Kettle is inactive / range warning in dashboard (only Kettle or Fermenter to be selected)"),
Property.Fermenter(label="Fermenter", description="Reduced logging if Fermenter is inactive / range warning in dashboard (only Kettle or Fermenter to be selected)"),
Property.Number(label="Offset", configurable=True, description="Offset for MQTT Sensor (default is 0). !!! Use this only with caution as offset for MQTT sensor should be defined on Sensor side !!!"),
Property.Number(label="ReducedLogging", configurable=True, description="Reduced logging frequency in seconds if selected Kettle or Fermenter is inactive (default:60 sec | 0 disabled)"),
Property.Number(label="Timeout", configurable=True, unit="sec",
description="Timeout in seconds to send notification (default:60 | deactivated: 0)"),
Property.Number(label="TempRange", configurable=True, unit="degree",
description="Temp range in degree between reading and target temp of fermenter/kettle. Larger difference shows different color in dashboard (default:0 | deactivated: 0)")])
class MQTTSensorOffset(CBPiSensor):
def __init__(self, cbpi, id, props):
super(MQTTSensorOffset, self).__init__(cbpi, id, props)
self.Topic = self.props.get("Topic", None)
self.offset = float(self.props.get("Offset", 0))
self.payload_text = self.props.get("PayloadDictionary", None)
if self.payload_text != None:
self.payload_text = self.payload_text.split('.')
self.subscribed = self.cbpi.satellite.subscribe(self.Topic, self.on_message)
self.value: float = 999
self.timeout=int(self.props.get("Timeout", 60))
self.temprange=float(self.props.get("TempRange", 0))
self.starttime = time.time()
self.notificationsend = False
self.nextchecktime=self.starttime+self.timeout
self.lastdata=time.time()
self.lastlog=0
self.sensor=self.get_sensor(self.id)
self.reducedfrequency=int(self.props.get("ReducedLogging", 60))
if self.reducedfrequency < 0:
self.reducedfrequency = 0
self.kettleid=self.props.get("Kettle", None)
self.fermenterid=self.props.get("Fermenter", None)
self.reducedlogging = True if self.kettleid or self.fermenterid else False
if self.kettleid is not None and self.fermenterid is not None:
self.reducedlogging=False
self.cbpi.notify("MQTTSensor", "Sensor '" + str(self.sensor.name) + "' cant't have Fermenter and Kettle defined for reduced logging / range warning.", NotificationType.WARNING, action=[NotificationAction("OK", self.Confirm)])
async def Confirm(self, **kwargs):
self.nextchecktime = time.time() + self.timeout
self.notificationsend = False
pass
async def message(self):
target_timestring= datetime.fromtimestamp(self.lastdata)
self.cbpi.notify("MQTTSensor Timeout", "Sensor '" + str(self.sensor.name) + "' did not respond. Last data received: "+target_timestring.strftime("%D %H:%M"), NotificationType.WARNING, action=[NotificationAction("OK", self.Confirm)])
pass
async def on_message(self, message):
val = json.loads(message.payload.decode())
try:
if self.payload_text is not None:
for key in self.payload_text:
val = val.get(key, None)
if isinstance(val, (int, float, str)):
self.value = float(val)+self.offset
self.push_update(self.value)
if self.reducedlogging == True:
await self.logvalue()
else:
logging.info("MQTTSensor {} regular logging".format(self.sensor.name))
self.log_data(self.value)
self.lastlog = time.time()
if self.timeout !=0:
self.nextchecktime = time.time() + self.timeout
self.notificationsend = False
self.lastdata=time.time()
except Exception as e:
logging.error("MQTT Sensor Error {}".format(e))
async def logvalue(self):
self.kettle = self.get_kettle(self.kettleid) if self.kettleid is not None else None
self.fermenter = self.get_fermenter(self.fermenterid) if self.fermenterid is not None else None
now=time.time()
if self.kettle is not None:
try:
kettlestatus=self.kettle.instance.state
except:
kettlestatus=False
if kettlestatus:
self.log_data(self.value)
logging.info("MQTTSensor {} Kettle Active".format(self.sensor.name))
self.lastlog = time.time()
else:
logging.info("MQTTSensor {} Kettle Inactive".format(self.sensor.name))
if self.reducedfrequency != 0:
if now >= self.lastlog + self.reducedfrequency:
self.log_data(self.value)
self.lastlog = time.time()
logging.info("Logged with reduced freqency")
pass
if self.fermenter is not None:
try:
fermenterstatus=self.fermenter.instance.state
except:
fermenterstatus=False
if fermenterstatus:
self.log_data(self.value)
logging.info("MQTTSensor {} Fermenter Active".format(self.sensor.name))
self.lastlog = time.time()
else:
logging.info("MQTTSensor {} Fermenter Inactive".format(self.sensor.name))
if self.reducedfrequency != 0:
if now >= self.lastlog + self.reducedfrequency:
self.log_data(self.value)
self.lastlog = time.time()
logging.info("Logged with reduced freqency")
pass
async def run(self):
while self.running:
if self.timeout !=0:
if time.time() > self.nextchecktime and self.notificationsend == False:
await self.message()
self.notificationsend=True
await asyncio.sleep(1)
def get_state(self):
return dict(value=self.value)
async def on_stop(self):
self.subscribed = self.cbpi.satellite.unsubscribe(self.Topic, self.on_message)
def setup(cbpi): def setup(cbpi):
''' '''
@ -145,4 +275,7 @@ def setup(cbpi):
:return: :return:
''' '''
if str(cbpi.static_config.get("mqtt", False)).lower() == "true": if str(cbpi.static_config.get("mqtt", False)).lower() == "true":
cbpi.plugin.register("MQTTSensor", MQTTSensor) if str(cbpi.static_config.get("mqtt_offset", False)).lower() == "false":
cbpi.plugin.register("MQTTSensor", MQTTSensor)
else:
cbpi.plugin.register("MQTTSensor", MQTTSensorOffset)

View file

@ -43,6 +43,7 @@ class SystemHttpEndpoints:
fermentersteps=self.cbpi.fermenter.get_fermenter_steps(), fermentersteps=self.cbpi.fermenter.get_fermenter_steps(),
config=self.cbpi.config.get_state(), config=self.cbpi.config.get_state(),
notifications=self.cbpi.notification.get_state(), notifications=self.cbpi.notification.get_state(),
bf_recipes=await self.cbpi.upload.get_brewfather_recipes(0),
version=__version__, version=__version__,
guiversion=version, guiversion=version,
codename=__codename__) codename=__codename__)

View file

@ -159,6 +159,23 @@ class UploadHttpEndpoints():
return web.json_response(bf_list) return web.json_response(bf_list)
@request_mapping(path='/bfupdate/', method="GET", auth_required=False)
async def get_bf_update(self, request):
"""
---
description: Get recipe list update from Brewfather App
tags:
- Upload
responses:
"200":
description: successful operation
"""
#offset = request.match_info['offset']
bf_list = await self.controller.get_brewfather_recipes()
self.cbpi.ws.send(dict(topic="bfupdate", data=bf_list))
return web.Response(status=200)
@request_mapping(path='/bf', method="POST", auth_required=False) @request_mapping(path='/bf', method="POST", auth_required=False)
async def create_bf_recipe(self, request): async def create_bf_recipe(self, request):
""" """

View file

@ -1,5 +1,5 @@
typing-extensions>=4 typing-extensions>=4
aiohttp==3.9.4 aiohttp==3.9.5
aiohttp-auth==0.1.1 aiohttp-auth==0.1.1
aiohttp-route-decorator==0.1.4 aiohttp-route-decorator==0.1.4
aiohttp-security==0.5.0 aiohttp-security==0.5.0
@ -8,7 +8,7 @@ aiohttp-swagger==1.0.16
async-timeout==4.0.3 async-timeout==4.0.3
aiojobs==1.2.1 aiojobs==1.2.1
aiosqlite==0.17.0 aiosqlite==0.17.0
cryptography==42.0.5 cryptography==42.0.8
pyopenssl==24.1.0 pyopenssl==24.1.0
requests==2.32.2 requests==2.32.2
voluptuous==0.14.2 voluptuous==0.14.2
@ -16,12 +16,12 @@ pyfiglet==1.0.2
pandas==2.2.2 pandas==2.2.2
shortuuid==1.0.13 shortuuid==1.0.13
tabulate==0.9.0 tabulate==0.9.0
numpy==1.26.4 numpy==2.0.0
cbpi4gui cbpi4gui
click==8.1.7 click==8.1.7
importlib_metadata==4.11.1 importlib_metadata==4.11.1
aiomqtt==2.1.0 aiomqtt==2.2.0
psutil==5.9.8 psutil==6.0.0
zipp>=0.5 zipp>=0.5
colorama==0.4.6 colorama==0.4.6
pytest-aiohttp pytest-aiohttp

View file

@ -39,7 +39,7 @@ setup(name='cbpi4',
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
install_requires=[ install_requires=[
"typing-extensions>=4", "typing-extensions>=4",
"aiohttp==3.9.4", "aiohttp==3.9.5",
"aiohttp-auth==0.1.1", "aiohttp-auth==0.1.1",
"aiohttp-route-decorator==0.1.4", "aiohttp-route-decorator==0.1.4",
"aiohttp-security==0.5.0", "aiohttp-security==0.5.0",
@ -48,7 +48,7 @@ setup(name='cbpi4',
"async-timeout==4.0.3", "async-timeout==4.0.3",
"aiojobs==1.2.1 ", "aiojobs==1.2.1 ",
"aiosqlite==0.17.0", "aiosqlite==0.17.0",
"cryptography==42.0.5", "cryptography==42.0.8",
"pyopenssl==24.1.0", "pyopenssl==24.1.0",
"requests==2.32.2", "requests==2.32.2",
"voluptuous==0.14.2", "voluptuous==0.14.2",
@ -56,13 +56,13 @@ setup(name='cbpi4',
'click==8.1.7', 'click==8.1.7',
'shortuuid==1.0.13', 'shortuuid==1.0.13',
'tabulate==0.9.0', 'tabulate==0.9.0',
'aiomqtt==2.1.0', 'aiomqtt==2.2.0',
'inquirer==3.2.4', 'inquirer==3.2.4',
'colorama==0.4.6', 'colorama==0.4.6',
'psutil==5.9.8', 'psutil==6.0.0',
'cbpi4gui', 'cbpi4gui',
'importlib_metadata', 'importlib_metadata',
'numpy==1.26.4', 'numpy==2.0.0',
'pandas==2.2.2'] + ( 'pandas==2.2.2'] + (
['rpi-lgpio'] if raspberrypi else [] ) + ( ['rpi-lgpio'] if raspberrypi else [] ) + (
['systemd-python'] if localsystem == "Linux" else [] ), ['systemd-python'] if localsystem == "Linux" else [] ),