BAPSicle/helpers/state_manager.py

192 lines
6.4 KiB
Python
Raw Normal View History

import json
import os
2020-12-19 14:57:37 +00:00
from logging import CRITICAL, INFO
import time
from datetime import datetime
from copy import copy
2021-04-08 21:21:28 +00:00
from typing import Any, List
2020-11-05 18:58:18 +00:00
from plan import PlanItem
2020-11-05 18:58:18 +00:00
from helpers.logging_manager import LoggingManager
from helpers.os_environment import resolve_external_file_path
2020-10-24 13:44:26 +00:00
2021-04-08 19:53:51 +00:00
2020-10-24 13:44:26 +00:00
class StateManager:
filepath: str
logger: LoggingManager
callbacks: List[Any] = []
__state = {}
__state_in_file = {}
# Dict of times that params can be updated after, if the time is before current time, it can be written immediately.
__rate_limit_params_until = {}
__rate_limit_period_s = 0
2021-04-08 19:53:51 +00:00
def __init__(
self,
name,
logger: LoggingManager,
default_state=None,
rate_limit_params=[],
rate_limit_period_s=5,
):
2020-10-30 23:59:58 +00:00
self.logger = logger
2021-04-10 23:11:05 +00:00
path_dir: str = resolve_external_file_path("/state")
if not os.path.isdir(path_dir):
try:
# Try creating the directory.
os.mkdir(path_dir)
except Exception:
print("Failed to create state directory.")
return
self.filepath = resolve_external_file_path("/state/" + name + ".json")
2020-10-30 23:59:58 +00:00
self._log("State file path set to: " + self.filepath)
if not os.path.isfile(self.filepath):
2020-10-30 23:59:58 +00:00
self._log("No existing state file found.")
try:
# Try creating the file.
open(self.filepath, "x")
2021-04-08 21:32:16 +00:00
except Exception:
2020-12-19 14:57:37 +00:00
self._log("Failed to create state file.", CRITICAL)
return
2021-04-08 19:53:51 +00:00
with open(self.filepath, "r") as file:
2020-10-30 23:59:58 +00:00
file_state = file.read()
if file_state == "":
2020-10-30 23:59:58 +00:00
self._log("State file is empty. Setting default state.")
2020-11-05 18:58:18 +00:00
self.state = default_state
self.__state_in_file = copy(self.state)
else:
2020-10-30 23:59:58 +00:00
try:
2020-11-05 18:58:18 +00:00
file_state = json.loads(file_state)
# Turn from JSON -> PlanItem
2020-11-09 00:10:36 +00:00
if "channel" in file_state:
2021-04-08 19:53:51 +00:00
file_state["loaded_item"] = (
PlanItem(file_state["loaded_item"])
if file_state["loaded_item"]
else None
)
file_state["show_plan"] = [
PlanItem(obj) for obj in file_state["show_plan"]
]
2020-11-05 18:58:18 +00:00
# Now feed the loaded state into the initialised state manager.
self.state = file_state
2021-04-08 21:32:16 +00:00
except Exception:
2021-04-08 19:53:51 +00:00
self._logException(
"Failed to parse state JSON. Resetting to default state."
)
2020-11-05 18:58:18 +00:00
self.state = default_state
self.__state_in_file = copy(self.state)
# Now setup the rate limiting
# Essentially rate limit all values to "now" to start with, allowing the first update
# of all vars to succeed.
for param in rate_limit_params:
self.__rate_limit_params_until[param] = self._currentTimeS
self.__rate_limit_period_s = rate_limit_period_s
@property
def state(self):
return copy(self.__state)
@state.setter
def state(self, state):
self.__state = copy(state)
2020-11-09 00:10:36 +00:00
def write_to_file(self, state):
if self.__state_in_file == state:
# No change to be updated.
return
self.__state_in_file = state
# Make sure we're not manipulating state
state_to_json = copy(state)
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
state_to_json["last_updated"] = current_time
# Not the biggest fan of this, but maybe I'll get a better solution for this later
2020-11-09 00:10:36 +00:00
if "channel" in state_to_json: # If its a channel object
2021-04-08 19:53:51 +00:00
state_to_json["loaded_item"] = (
state_to_json["loaded_item"].__dict__
if state_to_json["loaded_item"]
else None
)
state_to_json["show_plan"] = [
repr.__dict__ for repr in state_to_json["show_plan"]
]
2020-10-30 23:59:58 +00:00
try:
state_json = json.dumps(state_to_json, indent=2, sort_keys=True)
2021-04-08 21:32:16 +00:00
except Exception:
2020-10-30 23:59:58 +00:00
self._logException("Failed to dump JSON state.")
else:
with open(self.filepath, "w") as file:
file.write(state_json)
2020-12-19 14:57:37 +00:00
def update(self, key: str, value: Any, index: int = -1):
update_file = True
2021-04-08 19:53:51 +00:00
if key in self.__rate_limit_params_until.keys():
# The key we're trying to update is expected to be updating very often,
# We're therefore going to check before saving it.
if self.__rate_limit_params_until[key] > self._currentTimeS:
update_file = False
else:
2021-04-08 19:53:51 +00:00
self.__rate_limit_params_until[key] = (
self._currentTimeS + self.__rate_limit_period_s
)
state_to_update = self.state
if key in state_to_update and index == -1 and state_to_update[key] == value:
# We're trying to update the state with the same value.
# In this case, ignore the update
return
if index > -1 and key in state_to_update:
if not isinstance(state_to_update[key], list):
return
list_items = state_to_update[key]
if index >= len(list_items):
return
list_items[index] = value
state_to_update[key] = list_items
else:
state_to_update[key] = value
self.state = state_to_update
2021-04-08 21:21:28 +00:00
if update_file:
# Either a routine write, or state has changed.
# Update the file
self.write_to_file(state_to_update)
# Now tell any callback functions.
for callback in self.callbacks:
try:
callback()
2021-04-08 21:48:38 +00:00
except Exception as e:
2021-04-08 19:53:51 +00:00
self.logger.log.critical(
"Failed to execute status callback: {}".format(e)
)
def add_callback(self, function):
self._log("Adding callback: {}".format(str(function)))
self.callbacks.append(function)
2021-04-08 19:53:51 +00:00
def _log(self, text: str, level: int = INFO):
2020-10-30 23:59:58 +00:00
self.logger.log.log(level, "State Manager: " + text)
2021-04-08 19:53:51 +00:00
def _logException(self, text: str):
2020-10-30 23:59:58 +00:00
self.logger.log.exception("State Manager: " + text)
@property
def _currentTimeS(self):
return time.time()