mirror of
https://github.com/PiBrewing/craftbeerpi4.git
synced 2024-11-21 22:48:16 +01:00
Merge pull request #129 from PiBrewing/development
Merge from Development
This commit is contained in:
commit
1f645a835c
12 changed files with 134 additions and 31 deletions
|
@ -1,3 +1,3 @@
|
||||||
__version__ = "4.3.1"
|
__version__ = "4.3.2"
|
||||||
__codename__ = "Winter Storm"
|
__codename__ = "Winter Storm"
|
||||||
|
|
||||||
|
|
|
@ -107,16 +107,24 @@ class ConfigFolder:
|
||||||
for checking in required_config_content:
|
for checking in required_config_content:
|
||||||
if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self.configFolderPath, checking[0]), checking[1])):
|
if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self.configFolderPath, checking[0]), checking[1])):
|
||||||
# since there is no complete config we now check if the config folder may be completely empty to show hints:
|
# since there is no complete config we now check if the config folder may be completely empty to show hints:
|
||||||
if len(os.listdir(os.path.join(self.configFolderPath))) == 0 :
|
try:
|
||||||
print("***************************************************")
|
if len(os.listdir(os.path.join(self.configFolderPath))) == 0 :
|
||||||
print(f"the config folder '{self.configFolderPath}' seems to be completely empty")
|
print("***************************************************")
|
||||||
print("you might want to run 'cbpi setup'.print")
|
print(f"the config folder '{self.configFolderPath}' seems to be completely empty")
|
||||||
print("but you could also place your zipped config backup named")
|
print("you might want to run 'cbpi setup'.print")
|
||||||
print("'restored_config.zip' inside the mentioned config folder for")
|
print("but you could also place your zipped config backup named")
|
||||||
print("cbpi4 to automatically unpack it")
|
print("'restored_config.zip' inside the mentioned config folder for")
|
||||||
print("of course you can also place your config files manually")
|
print("cbpi4 to automatically unpack it")
|
||||||
print("***************************************************")
|
print("of course you can also place your config files manually")
|
||||||
return False
|
print("***************************************************")
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
print("***************************************************")
|
||||||
|
print("Cannot find config folder!")
|
||||||
|
print("Please navigate to path where you did run 'cbpi setup'.")
|
||||||
|
print("Or run 'cbpi setup' before starting the server.")
|
||||||
|
print("***************************************************")
|
||||||
|
return False
|
||||||
|
|
||||||
# if cbpi_dashboard_1.json does'nt exist at the new location (configFolderPath/dashboard)
|
# if cbpi_dashboard_1.json does'nt exist at the new location (configFolderPath/dashboard)
|
||||||
# we move every cbpi_dashboard_n.json file from the old location (configFolderPath) there.
|
# we move every cbpi_dashboard_n.json file from the old location (configFolderPath) there.
|
||||||
|
@ -145,8 +153,11 @@ class ConfigFolder:
|
||||||
# Starting with cbpi 4.2.0, the craftbeerpi.service file will be created dynamically from the template file based on the user id.
|
# Starting with cbpi 4.2.0, the craftbeerpi.service file will be created dynamically from the template file based on the user id.
|
||||||
# Therefore, the service file is replaced with a template file in the config folder
|
# Therefore, the service file is replaced with a template file in the config folder
|
||||||
if whatsmissing.find("craftbeerpi.template"):
|
if whatsmissing.find("craftbeerpi.template"):
|
||||||
self.copyDefaultFileIfNotExists("craftbeerpi.template")
|
try:
|
||||||
return False
|
self.copyDefaultFileIfNotExists("craftbeerpi.template")
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
pass
|
||||||
print("***************************************************")
|
print("***************************************************")
|
||||||
print(f"CraftBeerPi config content not found: {whatsmissing}")
|
print(f"CraftBeerPi config content not found: {whatsmissing}")
|
||||||
print("Please run 'cbpi setup' before starting the server ")
|
print("Please run 'cbpi setup' before starting the server ")
|
||||||
|
|
|
@ -525,13 +525,19 @@ class FermentationController:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
|
|
||||||
|
def remove_key(self,d, key):
|
||||||
|
r = dict(d)
|
||||||
|
del r[key]
|
||||||
|
return r
|
||||||
|
|
||||||
def push_update(self, key="fermenterupdate"):
|
def push_update(self, key="fermenterupdate"):
|
||||||
|
|
||||||
if key == self.update_key:
|
if key == self.update_key:
|
||||||
self.cbpi.ws.send(dict(topic=key, data=list(map(lambda item: item.to_dict(), self.data))))
|
self.cbpi.ws.send(dict(topic=key, data=list(map(lambda item: item.to_dict(), self.data))))
|
||||||
|
|
||||||
for item in self.data:
|
for item in self.data:
|
||||||
self.cbpi.push_update("cbpi/{}/{}".format(self.update_key,item.id), item.to_dict())
|
fermenters=self.remove_key(item.to_dict(),"steps")
|
||||||
|
self.cbpi.push_update("cbpi/{}/{}".format(self.update_key,item.id), fermenters)
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
fermentersteps=self.get_fermenter_steps()
|
fermentersteps=self.get_fermenter_steps()
|
||||||
|
@ -539,9 +545,20 @@ class FermentationController:
|
||||||
|
|
||||||
# send mqtt update for active femrentersteps
|
# send mqtt update for active femrentersteps
|
||||||
for fermenter in fermentersteps:
|
for fermenter in fermentersteps:
|
||||||
|
active = False
|
||||||
for step in fermenter['steps']:
|
for step in fermenter['steps']:
|
||||||
if step['status'] == 'A':
|
if step['status'] == 'A':
|
||||||
self.cbpi.push_update("cbpi/{}/{}/{}".format(key,fermenter['id'],step['id']), step)
|
active=True
|
||||||
|
active_step=step
|
||||||
|
# self.cbpi.push_update("cbpi/{}/{}/{}".format(key,fermenter['id'],step['id']), step)
|
||||||
|
#else:
|
||||||
|
# self.cbpi.push_update("cbpi/{}/{}".format(key,fermenter['id']), "")
|
||||||
|
if active:
|
||||||
|
self.cbpi.push_update("cbpi/{}/{}".format(key,fermenter['id']), active_step)
|
||||||
|
else:
|
||||||
|
self.cbpi.push_update("cbpi/{}/{}".format(key,fermenter['id']), "")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def call_action(self, id, action, parameter) -> None:
|
async def call_action(self, id, action, parameter) -> None:
|
||||||
logging.info("FermenterStep Controller - call Action {} {}".format(id, action))
|
logging.info("FermenterStep Controller - call Action {} {}".format(id, action))
|
||||||
|
|
|
@ -35,6 +35,11 @@ class SatelliteController:
|
||||||
]
|
]
|
||||||
self.tasks = set()
|
self.tasks = set()
|
||||||
|
|
||||||
|
def remove_key(self,d, key):
|
||||||
|
r = dict(d)
|
||||||
|
del r[key]
|
||||||
|
return r
|
||||||
|
|
||||||
async def init(self):
|
async def init(self):
|
||||||
|
|
||||||
#not sure if required like done in the old routine
|
#not sure if required like done in the old routine
|
||||||
|
@ -132,7 +137,8 @@ class SatelliteController:
|
||||||
try:
|
try:
|
||||||
self.fermenter=self.fermentercontroller.get_state()
|
self.fermenter=self.fermentercontroller.get_state()
|
||||||
for item in self.fermenter['data']:
|
for item in self.fermenter['data']:
|
||||||
self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item)
|
item_new=self.remove_key(item,"steps")
|
||||||
|
self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item_new)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning("Failed to send fermenterupdate via mqtt: {}".format(e))
|
self.logger.warning("Failed to send fermenterupdate via mqtt: {}".format(e))
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from os import listdir
|
||||||
import os
|
import os
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from cbpi.api.dataclasses import NotificationAction, Props, Step
|
from cbpi.api.dataclasses import NotificationAction, NotificationType, Props, Step
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from ..api.step import StepMove, StepResult, StepState
|
from ..api.step import StepMove, StepResult, StepState
|
||||||
|
@ -156,14 +156,14 @@ class StepController:
|
||||||
logging.info("BREWING COMPLETE")
|
logging.info("BREWING COMPLETE")
|
||||||
|
|
||||||
async def previous(self):
|
async def previous(self):
|
||||||
logging.info("Trigger Next")
|
logging.info("Trigger Previous")
|
||||||
|
|
||||||
|
|
||||||
async def next(self):
|
async def next(self):
|
||||||
logging.info("Trigger Next")
|
logging.info("Trigger Next")
|
||||||
print("\n\n\n\n")
|
#print("\n\n\n\n")
|
||||||
print(self.profile)
|
#print(self.profile)
|
||||||
print("\n\n\n\n")
|
#print("\n\n\n\n")
|
||||||
step = self.find_by_status(StepState.ACTIVE)
|
step = self.find_by_status(StepState.ACTIVE)
|
||||||
if step is not None:
|
if step is not None:
|
||||||
if step.instance is not None:
|
if step.instance is not None:
|
||||||
|
@ -299,6 +299,7 @@ class StepController:
|
||||||
await step.instance.start()
|
await step.instance.start()
|
||||||
step.status = StepState.ACTIVE
|
step.status = StepState.ACTIVE
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.cbpi.notify("Error", "Can't start step. Please check step in Mash Profile", NotificationType.ERROR)
|
||||||
logging.error("Failed to start step %s" % step)
|
logging.error("Failed to start step %s" % step)
|
||||||
|
|
||||||
async def save_basic(self, data):
|
async def save_basic(self, data):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import pkgutil
|
||||||
import psutil
|
import psutil
|
||||||
import pathlib
|
import pathlib
|
||||||
import json
|
import json
|
||||||
|
@ -12,6 +13,8 @@ from cbpi.api.config import ConfigType
|
||||||
from cbpi.api import *
|
from cbpi.api import *
|
||||||
import zipfile
|
import zipfile
|
||||||
import socket
|
import socket
|
||||||
|
import importlib
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
class SystemController:
|
class SystemController:
|
||||||
|
|
||||||
|
@ -41,6 +44,26 @@ class SystemController:
|
||||||
dir_name = pathlib.Path(self.cbpi.config_folder.get_file_path(''))
|
dir_name = pathlib.Path(self.cbpi.config_folder.get_file_path(''))
|
||||||
shutil.make_archive(output_filename, 'zip', dir_name)
|
shutil.make_archive(output_filename, 'zip', dir_name)
|
||||||
|
|
||||||
|
async def plugins_list(self):
|
||||||
|
result = []
|
||||||
|
discovered_plugins = {
|
||||||
|
name: importlib.import_module(name)
|
||||||
|
for finder, name, ispkg
|
||||||
|
in pkgutil.iter_modules()
|
||||||
|
if name.startswith('cbpi') and len(name) > 4
|
||||||
|
}
|
||||||
|
for key, module in discovered_plugins.items():
|
||||||
|
from importlib.metadata import version
|
||||||
|
try:
|
||||||
|
from importlib.metadata import (distribution, metadata,
|
||||||
|
version)
|
||||||
|
meta = metadata(key)
|
||||||
|
result.append(dict(Name=meta["Name"], Version=meta["Version"], Author=meta["Author"], Homepage=meta["Home-page"], Summary=meta["Summary"]))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return tabulate(result, headers="keys")
|
||||||
|
|
||||||
async def downloadlog(self, logtime):
|
async def downloadlog(self, logtime):
|
||||||
filename = "cbpi4.log"
|
filename = "cbpi4.log"
|
||||||
fullname = pathlib.Path(os.path.join(".",filename))
|
fullname = pathlib.Path(os.path.join(".",filename))
|
||||||
|
@ -60,7 +83,12 @@ class SystemController:
|
||||||
else:
|
else:
|
||||||
os.system('journalctl --since \"{} hours ago\" -u craftbeerpi.service --output cat > {}'.format(logtime, fullname))
|
os.system('journalctl --since \"{} hours ago\" -u craftbeerpi.service --output cat > {}'.format(logtime, fullname))
|
||||||
|
|
||||||
os.system('cbpi plugins > {}'.format(fullpluginname))
|
plugins = await self.plugins_list()
|
||||||
|
|
||||||
|
with open(fullpluginname, 'w') as f:
|
||||||
|
f.write(plugins)
|
||||||
|
|
||||||
|
#os.system('echo "{}" >> {}'.format(plugins,fullpluginname))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actors = self.cbpi.actor.get_state()
|
actors = self.cbpi.actor.get_state()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
import socket
|
||||||
try:
|
try:
|
||||||
from asyncio import set_event_loop_policy, WindowsSelectorEventLoopPolicy
|
from asyncio import set_event_loop_policy, WindowsSelectorEventLoopPolicy
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -277,6 +278,22 @@ class CraftBeerPi:
|
||||||
self.app.add_routes([web.get('/', http_index),
|
self.app.add_routes([web.get('/', http_index),
|
||||||
web.static('/static', os.path.join(os.path.dirname(__file__), "static"), show_index=True)])
|
web.static('/static', os.path.join(os.path.dirname(__file__), "static"), show_index=True)])
|
||||||
|
|
||||||
|
def testport(self, port=8000):
|
||||||
|
HOST = "localhost"
|
||||||
|
# Creates a new socket
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
# Try to connect to the given host and port
|
||||||
|
if sock.connect_ex((HOST, port)) == 0:
|
||||||
|
#print("Port " + str(port) + " is open") # Connected successfully
|
||||||
|
isrunning = True
|
||||||
|
else:
|
||||||
|
#print("Port " + str(port) + " is closed") # Failed to connect because port is in use (or bad host)
|
||||||
|
isrunning = False
|
||||||
|
# Close the connection
|
||||||
|
sock.close()
|
||||||
|
return isrunning
|
||||||
|
|
||||||
async def init_serivces(self):
|
async def init_serivces(self):
|
||||||
|
|
||||||
self._print_logo()
|
self._print_logo()
|
||||||
|
@ -304,4 +321,9 @@ class CraftBeerPi:
|
||||||
return self.app
|
return self.app
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
web.run_app(self.init_serivces(), port=self.static_config.get("port", 2202))
|
port=self.static_config.get("port",8000)
|
||||||
|
if not self.testport(port):
|
||||||
|
web.run_app(self.init_serivces(), port=port)
|
||||||
|
else:
|
||||||
|
logging.error("Port {} is already in use! Please check, if server is already running (e.g. in automode)".format(port))
|
||||||
|
exit(1)
|
|
@ -46,6 +46,8 @@ class FermenterAutostart(CBPiExtension):
|
||||||
Property.Number(label="HeaterOffsetOff", configurable=True, description="Offset as decimal number when the heater is switched off. Should be smaller then 'HeaterOffsetOn'. For example a value of 1 switches off the heater if the current temperature is 1 degree below the target temperature"),
|
Property.Number(label="HeaterOffsetOff", configurable=True, description="Offset as decimal number when the heater is switched off. Should be smaller then 'HeaterOffsetOn'. For example a value of 1 switches off the heater if the current temperature is 1 degree below the target temperature"),
|
||||||
Property.Number(label="CoolerOffsetOn", configurable=True, description="Offset as decimal number when the cooler is switched on. Should be greater then 'CoolerOffsetOff'. For example a value of 2 switches on the cooler if the current temperature is 2 degrees above the target temperature"),
|
Property.Number(label="CoolerOffsetOn", configurable=True, description="Offset as decimal number when the cooler is switched on. Should be greater then 'CoolerOffsetOff'. For example a value of 2 switches on the cooler if the current temperature is 2 degrees above the target temperature"),
|
||||||
Property.Number(label="CoolerOffsetOff", configurable=True, description="Offset as decimal number when the cooler is switched off. Should be smaller then 'CoolerOffsetOn'. For example a value of 1 switches off the cooler if the current temperature is 1 degree above the target temperature"),
|
Property.Number(label="CoolerOffsetOff", configurable=True, description="Offset as decimal number when the cooler is switched off. Should be smaller then 'CoolerOffsetOn'. For example a value of 1 switches off the cooler if the current temperature is 1 degree above the target temperature"),
|
||||||
|
Property.Number(label="HeaterMaxPower", configurable=True,description="Max Power [%] for Heater (default: 100)"),
|
||||||
|
Property.Number(label="CoolerMaxPower", configurable=True ,description="Max Power [%] for Cooler (default: 100)"),
|
||||||
Property.Select(label="AutoStart", options=["Yes","No"],description="Autostart Fermenter on cbpi start"),
|
Property.Select(label="AutoStart", options=["Yes","No"],description="Autostart Fermenter on cbpi start"),
|
||||||
Property.Sensor(label="sensor2",description="Optional Sensor for LCDisplay(e.g. iSpindle)")])
|
Property.Sensor(label="sensor2",description="Optional Sensor for LCDisplay(e.g. iSpindle)")])
|
||||||
|
|
||||||
|
@ -57,6 +59,8 @@ class FermenterHysteresis(CBPiFermenterLogic):
|
||||||
self.heater_offset_max = float(self.props.get("HeaterOffsetOff", 0))
|
self.heater_offset_max = float(self.props.get("HeaterOffsetOff", 0))
|
||||||
self.cooler_offset_min = float(self.props.get("CoolerOffsetOn", 0))
|
self.cooler_offset_min = float(self.props.get("CoolerOffsetOn", 0))
|
||||||
self.cooler_offset_max = float(self.props.get("CoolerOffsetOff", 0))
|
self.cooler_offset_max = float(self.props.get("CoolerOffsetOff", 0))
|
||||||
|
self.heater_max_power = int(self.props.get("HeaterMaxPower", 100))
|
||||||
|
self.cooler_max_power = int(self.props.get("CoolerMaxPower", 100))
|
||||||
|
|
||||||
self.fermenter = self.get_fermenter(self.id)
|
self.fermenter = self.get_fermenter(self.id)
|
||||||
self.heater = self.fermenter.heater
|
self.heater = self.fermenter.heater
|
||||||
|
@ -81,7 +85,7 @@ class FermenterHysteresis(CBPiFermenterLogic):
|
||||||
|
|
||||||
if sensor_value + self.heater_offset_min <= target_temp:
|
if sensor_value + self.heater_offset_min <= target_temp:
|
||||||
if self.heater and (heater_state == False):
|
if self.heater and (heater_state == False):
|
||||||
await self.actor_on(self.heater)
|
await self.actor_on(self.heater, self.heater_max_power)
|
||||||
|
|
||||||
if sensor_value + self.heater_offset_max >= target_temp:
|
if sensor_value + self.heater_offset_max >= target_temp:
|
||||||
if self.heater and (heater_state == True):
|
if self.heater and (heater_state == True):
|
||||||
|
@ -89,7 +93,7 @@ class FermenterHysteresis(CBPiFermenterLogic):
|
||||||
|
|
||||||
if sensor_value >= self.cooler_offset_min + target_temp:
|
if sensor_value >= self.cooler_offset_min + target_temp:
|
||||||
if self.cooler and (cooler_state == False):
|
if self.cooler and (cooler_state == False):
|
||||||
await self.actor_on(self.cooler)
|
await self.actor_on(self.cooler, self.cooler_max_power)
|
||||||
|
|
||||||
if sensor_value <= self.cooler_offset_max + target_temp:
|
if sensor_value <= self.cooler_offset_max + target_temp:
|
||||||
if self.cooler and (cooler_state == True):
|
if self.cooler and (cooler_state == True):
|
||||||
|
|
|
@ -37,6 +37,11 @@ class MQTTUtil(CBPiExtension):
|
||||||
self.push_update()
|
self.push_update()
|
||||||
await asyncio.sleep(self.mqttupdate)
|
await asyncio.sleep(self.mqttupdate)
|
||||||
|
|
||||||
|
def remove_key(self,d, key):
|
||||||
|
r = dict(d)
|
||||||
|
del r[key]
|
||||||
|
return r
|
||||||
|
|
||||||
def push_update(self):
|
def push_update(self):
|
||||||
# try:
|
# try:
|
||||||
# self.actor=self.actorcontroller.get_state()
|
# self.actor=self.actorcontroller.get_state()
|
||||||
|
@ -62,7 +67,8 @@ class MQTTUtil(CBPiExtension):
|
||||||
try:
|
try:
|
||||||
self.fermenter=self.fermentationcontroller.get_state()
|
self.fermenter=self.fermentationcontroller.get_state()
|
||||||
for item in self.fermenter['data']:
|
for item in self.fermenter['data']:
|
||||||
self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item)
|
item_new=self.remove_key(item,"steps")
|
||||||
|
self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item_new)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
typing-extensions>=4
|
typing-extensions>=4
|
||||||
aiohttp==3.9.1
|
aiohttp==3.9.3
|
||||||
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
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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.1",
|
"aiohttp==3.9.3",
|
||||||
"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",
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
"options": null,
|
"options": null,
|
||||||
"source": "hidden",
|
"source": "hidden",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"value": "4.2.0.a6"
|
"value": "4.3.2.a6"
|
||||||
},
|
},
|
||||||
"CSVLOGFILES": {
|
"CSVLOGFILES": {
|
||||||
"description": "Write sensor data to csv logfiles (enabling requires restart)",
|
"description": "Write sensor data to csv logfiles (enabling requires restart)",
|
||||||
|
@ -326,6 +326,14 @@
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"value": 1
|
"value": 1
|
||||||
},
|
},
|
||||||
|
"current_grid": {
|
||||||
|
"description": "Dashboard Grid Width",
|
||||||
|
"name": "current_grid",
|
||||||
|
"options": null,
|
||||||
|
"source": "hidden",
|
||||||
|
"type": "number",
|
||||||
|
"value": 5
|
||||||
|
},
|
||||||
"max_dashboard_number": {
|
"max_dashboard_number": {
|
||||||
"description": "Max Number of Dashboards",
|
"description": "Max Number of Dashboards",
|
||||||
"name": "max_dashboard_number",
|
"name": "max_dashboard_number",
|
||||||
|
|
Loading…
Reference in a new issue