Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AppSettings, set update interval #104

Merged
merged 3 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion IoTuring/ClassManager/consts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Class keys:
KEY_ENTITY = "entity"
KEY_WAREHOUSE = "warehouse"
KEY_SETTINGS = "settings"

CLASS_PATH = {
KEY_ENTITY: "Entity/Deployments",
KEY_WAREHOUSE: "Warehouse/Deployments"
KEY_WAREHOUSE: "Warehouse/Deployments",
KEY_SETTINGS: "Settings/Deployments"
}
8 changes: 4 additions & 4 deletions IoTuring/Configurator/Configuration.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from __future__ import annotations

from IoTuring.ClassManager.consts import KEY_ENTITY, KEY_WAREHOUSE
from IoTuring.ClassManager.consts import KEY_ENTITY, KEY_WAREHOUSE, KEY_SETTINGS

CONFIG_CLASS = {
KEY_ENTITY: "active_entities",
KEY_WAREHOUSE: "active_warehouses"
KEY_WAREHOUSE: "active_warehouses",
KEY_SETTINGS: "settings"
}

BLANK_CONFIGURATION = {
CONFIG_CLASS[KEY_ENTITY]: [{"type": "AppInfo"}],
CONFIG_CLASS[KEY_WAREHOUSE]: []
CONFIG_CLASS[KEY_ENTITY]: [{"type": "AppInfo"}]
}


Expand Down
47 changes: 40 additions & 7 deletions IoTuring/Configurator/Configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@

from IoTuring.Configurator.MenuPreset import QuestionPreset
from IoTuring.Configurator.Configuration import FullConfiguration, SingleConfiguration

from IoTuring.Logger.LogObject import LogObject
from IoTuring.Exceptions.Exceptions import UserCancelledException

from IoTuring.ClassManager.ClassManager import ClassManager, KEY_ENTITY, KEY_WAREHOUSE

from IoTuring.Configurator import ConfiguratorIO
from IoTuring.Configurator import messages

from IoTuring.ClassManager.ClassManager import ClassManager, KEY_ENTITY, KEY_WAREHOUSE, KEY_SETTINGS

from IoTuring.Logger.LogObject import LogObject
from IoTuring.Exceptions.Exceptions import UserCancelledException
from IoTuring.MyApp.SystemConsts import OperatingSystemDetection as OsD

from InquirerPy import inquirer
Expand Down Expand Up @@ -88,6 +86,7 @@ def Menu(self) -> None:
mainMenuChoices = [
{"name": "Manage entities", "value": self.ManageEntities},
{"name": "Manage warehouses", "value": self.ManageWarehouses},
{"name": "Settings", "value": self.ManageSettings},
{"name": "Start IoTuring", "value": self.WriteConfigurations},
{"name": "Help", "value": self.DisplayHelp},
{"name": "Quit", "value": self.Quit},
Expand Down Expand Up @@ -167,6 +166,39 @@ def ManageWarehouses(self) -> None:
else:
self.AddNewWarehouse(choice)

def ManageSettings(self) -> None:
""" UI for App and other Settings """

scm = ClassManager(KEY_SETTINGS)

choices = []

availableSettings = scm.ListAvailableClasses()
for sClass in availableSettings:

choices.append(
{"name": sClass.NAME + " Settings",
"value": sClass})

choice = self.DisplayMenu(
choices=choices,
message=f"Select settings to edit"
)

if choice == CHOICE_GO_BACK:
self.Menu()

else:
if not self.IsClassActive(choice):
self.config.configs.append(choice.GetDefaultConfigurations())

settings_config = self.config.GetConfigsOfType(choice.NAME)[0]

self.EditActiveConfiguration(
choice, settings_config)

self.ManageSettings()

def IsClassActive(self, typeClass) -> bool:
"""Check if class has an active configuration """
return bool(self.config.GetConfigsOfType(typeClass.NAME))
Expand Down Expand Up @@ -206,7 +238,8 @@ def AddNewWarehouse(self, whClass) -> None:

def ManageActiveConfiguration(self, typeClass, single_config: SingleConfiguration) -> None:
choices = [
{"name": f"Edit the {typeClass.GetClassKey()} settings", "value": "Edit"},
{"name": f"Edit the {typeClass.GetClassKey()} settings",
"value": "Edit"},
{"name": f"Remove the {typeClass.GetClassKey()}", "value": "Remove"}
]

Expand Down
39 changes: 35 additions & 4 deletions IoTuring/Configurator/ConfiguratorLoader.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from IoTuring.Entity.Entity import Entity
from IoTuring.Warehouse.Warehouse import Warehouse
from IoTuring.Settings.Settings import Settings

import sys

from IoTuring.Entity.Entity import Entity
from IoTuring.Logger.LogObject import LogObject
from IoTuring.Configurator.Configurator import Configurator
from IoTuring.ClassManager.ClassManager import ClassManager, KEY_ENTITY, KEY_WAREHOUSE
from IoTuring.Warehouse.Warehouse import Warehouse
from IoTuring.ClassManager.ClassManager import ClassManager, KEY_ENTITY, KEY_WAREHOUSE, KEY_SETTINGS


class ConfiguratorLoader(LogObject):
configurator = None

def __init__(self, configurator: Configurator) -> None:
self.configurations = configurator.config
Expand Down Expand Up @@ -64,3 +67,31 @@ def LoadEntities(self) -> list[Entity]:
# - for each one:
# - pass the configuration to the warehouse function that uses the configuration to init the Warehouse
# - append the Warehouse to the list

def LoadSettings(self) -> list[Settings]:
settings = []
scm = ClassManager(KEY_SETTINGS)
settingsClasses = scm.ListAvailableClasses()

for sClass in settingsClasses:

# Check if config was saved:
savedConfigs = self.configurations\
.GetConfigsOfType(sClass.NAME)

if savedConfigs:
sc = sClass(savedConfigs[0])

# Fallback to default:
else:
sc = sClass(sClass.GetDefaultConfigurations())

settings.append(sc)
return settings

# How Settings configurations work:
# - Settings' configs are added to SettingsManager singleton on main __init__
# - For advanced usage custom loading could be set up in the class' __init()__
# - Setting classes has classmethods to get their configs from SettingsManager Singleton,
# SettingsManager shouldn't be called directly, e.g:
# AppSettings.GetFromSettingsConfigurations(CONFIG_KEY_UPDATE_INTERVAL)
15 changes: 12 additions & 3 deletions IoTuring/Configurator/ConfiguratorObject.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from IoTuring.Configurator.MenuPreset import BooleanAnswers, MenuPreset
from IoTuring.Configurator.Configuration import SingleConfiguration, CONFIG_CLASS
from IoTuring.Configurator.Configuration import SingleConfiguration, CONFIG_CLASS, CONFIG_KEY_TYPE


class ConfiguratorObject:
Expand All @@ -11,8 +11,7 @@ def __init__(self, single_configuration: SingleConfiguration) -> None:
self.configurations = single_configuration

# Add missing default values:
preset = self.ConfigurationPreset()
defaults = preset.GetDefaults()
defaults = self.ConfigurationPreset().GetDefaults()

if defaults:
for default_key, default_value in defaults.items():
Expand Down Expand Up @@ -59,3 +58,13 @@ def GetClassKey(cls) -> str:
if class_key not in CONFIG_CLASS:
raise Exception(f"Invalid class {class_key}")
return class_key

@classmethod
def GetDefaultConfigurations(cls) -> SingleConfiguration:
"""Get the default configuration of this class"""

# Get default configs as dict:
config_dict = cls.ConfigurationPreset().GetDefaults()
config_dict[CONFIG_KEY_TYPE] = cls.NAME

return SingleConfiguration(cls.GetClassKey(), config_dict=config_dict)
17 changes: 12 additions & 5 deletions IoTuring/Entity/Entity.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from IoTuring.Configurator.Configuration import SingleConfiguration
from IoTuring.Entity.EntityData import EntityData, EntitySensor, EntityCommand, ExtraAttribute


import time
import subprocess

from IoTuring.Configurator.ConfiguratorObject import ConfiguratorObject
from IoTuring.Configurator.Configuration import SingleConfiguration
from IoTuring.Exceptions.Exceptions import UnknownEntityKeyException
from IoTuring.Logger.LogObject import LogObject
from IoTuring.Entity.EntityData import EntityData, EntitySensor, EntityCommand, ExtraAttribute
from IoTuring.Exceptions.Exceptions import UnknownEntityKeyException

from IoTuring.MyApp.SystemConsts import OperatingSystemDetection as OsD

DEFAULT_UPDATE_TIMEOUT = 10
from IoTuring.Settings.Deployments.AppSettings.AppSettings import AppSettings, CONFIG_KEY_UPDATE_INTERVAL


class Entity(ConfiguratorObject, LogObject):
Expand All @@ -25,7 +30,9 @@ def __init__(self, single_configuration: SingleConfiguration) -> None:

# When I update the values this number changes (randomly) so each warehouse knows I have updated
self.valuesID = 0
self.updateTimeout = DEFAULT_UPDATE_TIMEOUT

self.updateTimeout = int(
AppSettings.GetFromSettingsConfigurations(CONFIG_KEY_UPDATE_INTERVAL))

def Initialize(self):
""" Must be implemented in sub-classes, may be useful here to use the configuration """
Expand Down
32 changes: 32 additions & 0 deletions IoTuring/Settings/Deployments/AppSettings/AppSettings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from IoTuring.Configurator.MenuPreset import MenuPreset
from IoTuring.Settings.Settings import Settings


CONFIG_KEY_UPDATE_INTERVAL = "update_interval"
CONFIG_KEY_RETRY_INTERVAL = "retry_interval"
# CONFIG_KEY_SLOW_INTERVAL = "slow_interval"


class AppSettings(Settings):
"""Class that stores AppSettings, not related to a specifuc Entity or Warehouse """
NAME = "App"

@classmethod
def ConfigurationPreset(cls):
preset = MenuPreset()

preset.AddEntry(name="Main update interval in seconds",
key=CONFIG_KEY_UPDATE_INTERVAL, mandatory=True,
question_type="integer", default=10)

preset.AddEntry(name="Connection retry interval in seconds",
instruction="If broker is not available retry after this amount of time passed",
key=CONFIG_KEY_RETRY_INTERVAL, mandatory=True,
question_type="integer", default=1)


# preset.AddEntry(name="Secondary update interval in minutes",
# key=CONFIG_KEY_SLOW_INTERVAL, mandatory=True,
# question_type="integer", default=10)

return preset
45 changes: 45 additions & 0 deletions IoTuring/Settings/Settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from IoTuring.Configurator.ConfiguratorObject import ConfiguratorObject
from IoTuring.Settings.SettingsManager import SettingsManager
from IoTuring.Configurator.MenuPreset import BooleanAnswers


class Settings(ConfiguratorObject):
"""Base class for settings"""
NAME = "Settings"

@classmethod
def GetFromSettingsConfigurations(cls, key: str):
"""Get value from settings' saved configurations from SettingsManager

Args:
key (str): The CONFIG_KEY of the configuration

Raises:
Exception: If the key not found

Returns:
Any: The config value
"""

sM = SettingsManager()
saved_config = sM.GetConfigOfType(cls)

if saved_config.HasConfigKey(key):
return saved_config.GetConfigValue(key)
else:
raise Exception(
f"Can't find key {key} in SettingsManager configurations")

@classmethod
def GetTrueOrFalseFromSettingsConfigurations(cls, key: str) -> bool:
"""Get boolean value from settings' saved configurations from SettingsManager

Args:
key (str): The CONFIG_KEY of the configuration

Returns:
bool: The config value
"""

value = cls.GetFromSettingsConfigurations(key).lower()
return bool(value in BooleanAnswers.TRUE_ANSWERS)
32 changes: 32 additions & 0 deletions IoTuring/Settings/SettingsManager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from IoTuring.Settings.Settings import Settings
from IoTuring.Configurator.Configuration import SingleConfiguration

from IoTuring.Logger.Logger import Singleton
from IoTuring.Logger.LogObject import LogObject


class SettingsManager(LogObject, metaclass=Singleton):
"""Singleton for storing configurations of Settings"""

def __init__(self) -> None:
self.setting_configs = {}

def AddSettings(self, setting_entities: list[Settings]) -> None:
"""Add settings configuration

Args:
setting_entities (list[Settings]): The loaded settings classes
"""
for setting_entity in setting_entities:
self.setting_configs[setting_entity.NAME] = setting_entity.configurations

def GetConfigOfType(self, setting_class) -> SingleConfiguration:
"""Get the configuration of a saved class. Raises exception if not found"""

if setting_class.NAME in self.setting_configs:
return self.setting_configs[setting_class.NAME]
else:
raise Exception(f"No settings config for {setting_class.NAME}")
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
from IoTuring.Logger import consts
from IoTuring.Entity.ValueFormat import ValueFormatter

from IoTuring.Settings.Deployments.AppSettings.AppSettings import AppSettings, CONFIG_KEY_UPDATE_INTERVAL


INCLUDE_UNITS_IN_SENSORS = False
INCLUDE_UNITS_IN_EXTRA_ATTRIBUTES = True

SLEEP_TIME_NOT_CONNECTED_WHILE = 1

# That stands for: App name, Client name, EntityData Id
TOPIC_DATA_FORMAT = "{}/{}HomeAssistant/{}"
Expand Down Expand Up @@ -240,8 +241,12 @@ def __init__(self, entityData: EntitySensor, wh: "HomeAssistantWarehouse") -> N
if self.supports_extra_attributes:
self.AddTopic("json_attributes_topic")

# Extra payload for sensors:
self.discovery_payload['expire_after'] = 600 # TODO Improve
# Make sure expire_after is greater than the loop timeout:
loop_timeout = int(AppSettings.GetFromSettingsConfigurations(
CONFIG_KEY_UPDATE_INTERVAL))
sensor_expire_seconds = 600 if loop_timeout < 600 else int(
loop_timeout * 1.5)
self.discovery_payload['expire_after'] = sensor_expire_seconds

def SendValues(self):
""" Send values of the sensor to the state topic """
Expand Down Expand Up @@ -404,7 +409,7 @@ def RegisterEntityCommands(self):
def Loop(self):

while (not self.client.IsConnected()):
time.sleep(SLEEP_TIME_NOT_CONNECTED_WHILE)
time.sleep(self.retry_interval)

# Mechanism to call the function to send discovery data every CONFIGURATION_SEND_LOOP_SKIP_NUMBER loop
if self.loopCounter == 0:
Expand Down Expand Up @@ -434,7 +439,7 @@ def MakeValuesTopic(self, topic_suffix: str) -> str:
def NormalizeTopic(topicstring: str) -> str:
""" Home assistant requires stricter topic names """
# Remove non ascii characters:
topicstring=topicstring.encode("ascii", "ignore").decode()
topicstring = topicstring.encode("ascii", "ignore").decode()
return MQTTClient.NormalizeTopic(topicstring).replace(" ", "_")

@classmethod
Expand Down
Loading
Loading