2020-10-23 23:45:27 +00:00
import json
import os
2021-05-12 23:26:43 +00:00
from logging import CRITICAL , DEBUG , INFO
2020-11-04 01:19:56 +00:00
import time
from datetime import datetime
from copy import copy
2021-04-22 22:00:31 +00:00
from typing import Any , Dict , List
2020-11-05 18:58:18 +00:00
2021-04-12 21:59:51 +00:00
from baps_types . plan import PlanItem
2020-11-05 18:58:18 +00:00
from helpers . logging_manager import LoggingManager
2020-10-28 22:04:11 +00:00
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 :
2021-04-05 21:13:53 +00:00
filepath : str
logger : LoggingManager
2021-02-14 13:57:07 +00:00
callbacks : List [ Any ] = [ ]
2020-10-23 23:45:27 +00:00
__state = { }
2020-11-04 01:19:56 +00:00
# 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
2020-10-23 23:45:27 +00:00
2021-04-08 19:53:51 +00:00
def __init__ (
self ,
name ,
logger : LoggingManager ,
2021-04-22 22:00:31 +00:00
default_state : Dict [ str , Any ] = None ,
2021-04-08 19:53:51 +00:00
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
2020-10-28 22:04:11 +00:00
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 )
2020-10-23 23:45:27 +00:00
if not os . path . isfile ( self . filepath ) :
2020-10-30 23:59:58 +00:00
self . _log ( " No existing state file found. " )
2020-10-23 23:45:27 +00:00
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 )
2020-10-23 23:45:27 +00:00
return
2021-05-02 18:09:59 +00:00
file_raw : str
2021-04-08 19:53:51 +00:00
with open ( self . filepath , " r " ) as file :
2021-05-02 18:09:59 +00:00
file_raw = file . read ( )
2020-10-23 23:45:27 +00:00
2021-05-02 18:09:59 +00:00
if file_raw == " " :
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
2020-10-23 23:45:27 +00:00
else :
2020-10-30 23:59:58 +00:00
try :
2021-05-02 18:09:59 +00:00
file_state : Dict [ str , Any ] = json . loads ( file_raw )
2020-11-04 20:33:09 +00:00
2020-11-16 22:49:33 +00:00
# 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-04 20:33:09 +00:00
2020-11-05 18:58:18 +00:00
# Now feed the loaded state into the initialised state manager.
self . state = file_state
2021-04-22 22:00:31 +00:00
# If there are any new config options in the default state, save them.
# Uses update() to save them to file too.
for key in default_state . keys ( ) :
if not key in file_state . keys ( ) :
self . update ( key , default_state [ key ] )
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
2020-10-23 23:45:27 +00:00
2020-11-04 01:19:56 +00:00
# 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
2020-10-23 23:45:27 +00:00
@property
def state ( self ) :
2020-11-04 01:19:56 +00:00
return copy ( self . __state )
2020-10-23 23:45:27 +00:00
2021-04-18 19:27:54 +00:00
# Useful for pipeproxy, since it can't read attributes direct.
def get ( self ) :
return self . state
2020-10-23 23:45:27 +00:00
@state.setter
def state ( self , state ) :
2020-11-04 01:19:56 +00:00
self . __state = copy ( state )
2020-11-09 00:10:36 +00:00
def write_to_file ( self , state ) :
2020-11-04 01:19:56 +00:00
# Make sure we're not manipulating state
2020-11-04 20:33:09 +00:00
state_to_json = copy ( state )
2020-11-04 01:19:56 +00:00
now = datetime . now ( )
2021-05-12 23:26:43 +00:00
2020-11-04 01:19:56 +00:00
current_time = now . strftime ( " % H: % M: % S " )
2020-11-04 20:33:09 +00:00
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 :
2020-11-04 20:33:09 +00:00
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-10-23 23:45:27 +00:00
2020-12-19 14:57:37 +00:00
def update ( self , key : str , value : Any , index : int = - 1 ) :
2020-11-04 01:19:56 +00:00
update_file = True
2021-04-08 19:53:51 +00:00
if key in self . __rate_limit_params_until . keys ( ) :
2020-11-04 01:19:56 +00:00
# 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
)
2020-11-04 01:19:56 +00:00
state_to_update = self . state
2021-05-12 23:26:43 +00:00
if key in state_to_update and index == - 1 and state_to_update [ key ] == value :
allow = False
# It's hard to compare lists, especially of complex objects like show plans, just write it.
if ( isinstance ( value , list ) ) :
allow = True
# If the two objects have dict representations, and they don't match, allow writing.
# TODO: This should be easier.
if ( getattr ( value , " __dict__ " , None ) and getattr ( state_to_update [ key ] , " __dict__ " , None ) ) :
if value . __dict__ != state_to_update [ key ] . __dict__ :
allow = True
if not allow :
# Just some debug logging.
2021-09-01 22:40:28 +00:00
if update_file and ( key not in [ " playing " , " loaded " , " initialised " , " remaining " , " pos_true " ] ) :
2021-09-01 21:25:44 +00:00
self . _log ( " Not updating state for key ' {} ' with value ' {} ' of type ' {} ' . " . format ( key , value , type ( value ) ) , DEBUG )
2021-05-12 23:26:43 +00:00
# We're trying to update the state with the same value.
# In this case, ignore the update
# This happens to reduce spam on file writes / callbacks fired when update_file is true.
return
2020-11-04 01:19:56 +00:00
2020-12-08 19:41:11 +00:00
if index > - 1 and key in state_to_update :
if not isinstance ( state_to_update [ key ] , list ) :
2021-09-01 21:25:44 +00:00
self . _log ( " Not updating state for key ' {} ' with value ' {} ' of type ' {} ' since index is set and key is not a list. " . format ( key , value , type ( value ) ) , DEBUG )
2020-12-08 19:41:11 +00:00
return
list_items = state_to_update [ key ]
if index > = len ( list_items ) :
2021-09-01 21:25:44 +00:00
self . _log ( " Not updating state for key ' {} ' with value ' {} ' of type ' {} ' because index ' {} ' is too large.. " . format ( key , value , type ( value ) , index ) , DEBUG )
2020-12-08 19:41:11 +00:00
return
list_items [ index ] = value
state_to_update [ key ] = list_items
else :
state_to_update [ key ] = value
2020-11-04 01:19:56 +00:00
self . state = state_to_update
2021-04-08 21:21:28 +00:00
if update_file :
2021-09-01 21:25:44 +00:00
self . _log ( " Writing change to key ' {} ' with value ' {} ' of type ' {} ' to disk. " . format ( key , value , type ( value ) ) , DEBUG )
2021-02-14 13:57:07 +00:00
# Either a routine write, or state has changed.
# Update the file
2020-11-04 01:19:56 +00:00
self . write_to_file ( state_to_update )
2021-02-14 13:57:07 +00:00
# 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 )
)
2021-02-14 13:57:07 +00:00
def add_callback ( self , function ) :
2021-04-05 21:13:53 +00:00
self . _log ( " Adding callback: {} " . format ( str ( function ) ) )
2021-02-14 13:57:07 +00:00
self . callbacks . append ( function )
2020-10-23 23:45:27 +00:00
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 )
2020-11-04 01:19:56 +00:00
@property
def _currentTimeS ( self ) :
return time . time ( )