1212import sys
1313import shutil
1414import sqlite3
15+ from decimal import Decimal , InvalidOperation
1516from datetime import datetime
1617
1718__author__ = "Sebastian Hollas"
18- __version__ = "2.1.0 "
19+ __version__ = "2.1.1 "
1920
2021####################################################################################
2122# USER INPUT REQUIRED !
3536# Build Filepaths
3637ENTITIES_FILE = os .path .join (HA_CONFIG_ROOT , "entities.list" )
3738RESTORE_STATE_PATH = os .path .join (HA_CONFIG_ROOT , ".storage" , "core.restore_state" )
38- CONFIG_ENTRIES_PATH = os .path .join (HA_CONFIG_ROOT , ".storage" , "core.config_entries" )
39- ENTITY_REGISTRY_PATH = os .path .join (HA_CONFIG_ROOT , ".storage" , "core.entity_registry" )
4039
4140if not os .path .isfile (RESTORE_STATE_PATH ):
4241 sys .exit (f"File { RESTORE_STATE_PATH } does not exist! (Path to HomeAssistant config valid?)" )
4342
44- if not os .path .isfile (CONFIG_ENTRIES_PATH ):
45- sys .exit (f"File { CONFIG_ENTRIES_PATH } does not exist! (Path to HomeAssistant config valid?)" )
46-
47- if not os .path .isfile (ENTITY_REGISTRY_PATH ):
48- sys .exit (f"File { ENTITY_REGISTRY_PATH } does not exist! (Path to HomeAssistant config valid?)" )
49-
50-
5143# Open MySQL server connection if user provided DB_SERVER information
5244if all (DB_SERVER .values ()):
5345 import pymysql
5446 db = pymysql .connect (
5547 host = DB_SERVER ["DB_HOST" ],
5648 user = DB_SERVER ["DB_USER" ],
5749 password = DB_SERVER ["DB_PASSWORD" ],
58- database = DB_SERVER ["DB_NAME" ]
50+ database = DB_SERVER ["DB_NAME" ],
51+ autocommit = True
5952 )
6053
6154# Create connection to database file if no DB_SERVER information was provided
@@ -108,8 +101,11 @@ def main():
108101 with open (ENTITIES_FILE , "w" ) as file :
109102 # Get Entities that have a round option
110103 SqlExec ("SELECT statistic_id FROM statistics_meta WHERE has_sum=1" , ())
111- for entity_id in getEntitiesPrecision ():
112- file .write (f"{ entity_id } \n " )
104+ if not (result := cur .fetchall ()):
105+ sys .exit ("There are no entities which can be fixed in the database (key 'sum' in table statistics_meta is not populated)" )
106+
107+ for entity_id in result :
108+ file .write (f"{ entity_id [0 ]} \n " )
113109
114110 print (f"File '{ ENTITIES_FILE } ' created with entities that have the key 'sum'"
115111 f"\n Please adjust to your needs and rerun the script with no arguments." )
@@ -119,8 +115,6 @@ def main():
119115
120116
121117def fixDatabase (ENTITIES : list ):
122- # Get Precision of Entities
123- EntityPrecision = getEntitiesPrecision ()
124118
125119 # Fix value for all metadata_ids
126120 for entity_id in ENTITIES :
@@ -147,44 +141,31 @@ def fixDatabase(ENTITIES: list):
147141 # Get metadata_id from SQL Query result
148142 metadata_id_statistics = result [0 ]
149143
150- ################################################################################################################
151- # Get amount of decimals for Riemann Sum integral that the user configured
152- if entity_id not in EntityPrecision :
153- print (f" [WARNING]: Entity seems not to be a Riemann Sum Entity! UNTESTED. USE WITH CAUTION!" )
154- roundDigits = - 1
155- else :
156- # Get Precision of Entity that user configured
157- roundDigits = EntityPrecision [entity_id ]
158-
159144 ################################################################################################################
160145 # FIX DATABASE
161146 print ("\n ========================================================================" )
162147 print (f"{ entity_id } | { metadata_id_states = } | { metadata_id_statistics = } " )
163148
164149 # Fix table "statistics"
165- lastValidSum = recalculateStatistics (metadata_id = metadata_id_statistics , key = "sum" , roundDigits = roundDigits )
166- lastValidState = recalculateStatistics (metadata_id = metadata_id_statistics , key = "state" , roundDigits = roundDigits )
150+ lastValidSum = recalculateStatistics (metadata_id = metadata_id_statistics , key = "sum" )
151+ lastValidState = recalculateStatistics (metadata_id = metadata_id_statistics , key = "state" )
167152
168153 # Delete ShortTerm statistics and input one entry with current state
169154 fixShortTerm (metadata_id = metadata_id_statistics , lastValidSum = lastValidSum , lastValidState = lastValidState )
170155
171156 # Fix table "states"
172- recalculateStates (metadata_id = metadata_id_states , roundDigits = roundDigits )
157+ recalculateStates (metadata_id = metadata_id_states )
173158
174- # Fix last valid state if entity seems to be a Riemann Sum Entity only
175- # OPEN: How to find out if entity is a Riemann Sum Entity?!
176- # Currently: If entity is in table statistics and has a "round" attribute, it is assumed to be a Riemann Sum Entity
177- if roundDigits != - 1 :
178- # Fix last valid state in HA to ensure a valid calculation with the next Riemann Sum calculation
179- fixLastValidState (entity_id = entity_id , lastValidState = lastValidState )
159+ # Fix last valid state to current state
160+ fixLastValidState (entity_id = entity_id , lastValidState = lastValidState )
180161
181162 # Store database on disk
182163 print (f"\n { db .total_changes } changes made to database!" )
183164 db .commit ()
184165 db .close ()
185166
186167
187- def recalculateStatistics (metadata_id : int , key : str , roundDigits : int ) -> str :
168+ def recalculateStatistics (metadata_id : int , key : str ) -> str :
188169
189170 print (f" Fixing table statistics for key: { key } " )
190171
@@ -194,7 +175,7 @@ def recalculateStatistics(metadata_id: int, key: str, roundDigits: int) -> str:
194175
195176 # Get first value from database; this is our starting point
196177 try :
197- current_value = float ( result [0 ][1 ])
178+ current_value = Decimal ( str ( result [0 ][1 ]) )
198179 except ValueError :
199180 sys .exit (f" [ERROR]: Cannot fix this entity because first entry in table 'statistics' for { key } is not a number! Sorry!" )
200181
@@ -204,33 +185,27 @@ def recalculateStatistics(metadata_id: int, key: str, roundDigits: int) -> str:
204185 # Get previous entry
205186 _ , pre_value = result [index ]
206187
188+ # Convert do decimal object
189+ value = Decimal (str (value ))
190+ pre_value = Decimal (str (pre_value ))
191+
207192 if value < current_value :
208193 # Current value is out-dated
209194
210195 if value >= pre_value :
211196 # Recalculate new value with difference of previous entries
212197 current_value += (value - pre_value )
213198
214- if roundDigits != - 1 :
215- roundedValue = f"{ current_value :.{roundDigits }f} "
216- else :
217- # Just copy because we don't round the value
218- roundedValue = current_value
219- print (f" Updating { idx = } : { value = } -> { roundedValue = } " )
220- SqlExec (f"UPDATE statistics SET { key } =? WHERE id=?" , (roundedValue , idx ))
199+ print (f" Updating { idx = } : { value } -> { current_value } " )
200+ SqlExec (f"UPDATE statistics SET { key } =? WHERE id=?" , (float (current_value ), idx ))
221201
222202 continue
223203
224204 # Set current value as new value
225205 current_value = value
226206
227207 # Return last value
228- if roundDigits != - 1 :
229- # Return rounded value
230- return f"{ current_value :.{roundDigits }f} "
231- else :
232- # Return value as it is
233- return current_value
208+ return str (current_value )
234209
235210
236211def fixShortTerm (metadata_id : int , lastValidSum : str , lastValidState : str ):
@@ -249,7 +224,7 @@ def fixShortTerm(metadata_id: int, lastValidSum: str, lastValidState: str):
249224 (lastValidState , lastValidSum , metadata_id , now_end .timestamp (), now_start .timestamp ()))
250225
251226
252- def recalculateStates (metadata_id : int , roundDigits : int ):
227+ def recalculateStates (metadata_id : int ):
253228 print (f" Fixing table states" )
254229
255230 SqlExec ("SELECT state_id,state,old_state_id,attributes_id FROM states WHERE metadata_id=? ORDER BY state_id" ,
@@ -258,10 +233,10 @@ def recalculateStates(metadata_id: int, roundDigits: int):
258233
259234 # Get first value from database; this is our starting point
260235 try :
261- current_state = float ( result [0 ][1 ])
236+ current_state = Decimal ( str ( result [0 ][1 ]) )
262237 attributes_id = result [0 ][3 ]
263- except ValueError :
264- sys .exit (" [ERROR]: Cannot fix this entity because first entry in table 'states' is not a number! Sorry! " )
238+ except InvalidOperation :
239+ sys .exit (f " [ERROR]: Cannot fix this entity because first entry in table 'states' is not a number! first entry: { result [ 0 ][ 3 ] } " )
265240
266241 # Loop over all entries starting with the second entry
267242 for index , (state_id , state , old_state_id , attr_id ) in enumerate (result [1 :]):
@@ -277,30 +252,20 @@ def recalculateStates(metadata_id: int, roundDigits: int):
277252
278253 if state is None or not state .replace ("." , "" , 1 ).isdigit ():
279254 # State is NULL or not numeric; update to current value
280- if roundDigits != - 1 :
281- roundedValue = f"{ current_state :.{roundDigits }f} "
282- else :
283- # Just copy because we don't round the value
284- roundedValue = current_state
285- print (f" Updating { state_id = } : { state = } -> { roundedValue } " )
286- SqlExec ("UPDATE states SET state=? WHERE state_id=?" , (roundedValue , state_id ))
255+ print (f" Updating { state_id = } : { state } -> { current_state } " )
256+ SqlExec ("UPDATE states SET state=? WHERE state_id=?" , (float (current_state ), state_id ))
287257 continue
288258
289- state = float ( state )
259+ state = Decimal ( str ( state ) )
290260 if state < current_state :
291261 # Current value is out-dated
292262
293- if pre_state and pre_state .replace ("." , "" , 1 ).isdigit () and state >= float ( pre_state ):
263+ if pre_state and pre_state .replace ("." , "" , 1 ).isdigit () and state >= Decimal ( str ( pre_state ) ):
294264 # Recalculate new value with difference of previous entries
295- current_state += (state - float (pre_state ))
296-
297- if roundDigits != - 1 :
298- roundedValue = f"{ current_state :.{roundDigits }f} "
299- else :
300- # Just copy because we don't round the value
301- roundedValue = current_state
302- print (f" Updating { state_id = } : { state = } -> { roundedValue } " )
303- SqlExec ("UPDATE states SET state=? WHERE state_id=?" , (roundedValue , state_id ))
265+ current_state += (state - Decimal (str (pre_state )))
266+
267+ print (f" Updating { state_id = } : { state } -> { current_state } " )
268+ SqlExec ("UPDATE states SET state=? WHERE state_id=?" , (float (current_state ), state_id ))
304269 continue
305270
306271 # Set current value as new value
@@ -316,53 +281,26 @@ def fixLastValidState(entity_id: str, lastValidState: str):
316281 for state in restore_state ["data" ]:
317282
318283 # Search for entity_id
319- if state ["state" ]["entity_id" ] == entity_id :
320- # Modify state to new value
321- state ["state" ]["state" ] = lastValidState
322- state ["extra_data" ]["native_value" ]["decimal_str" ] = lastValidState
323- state ["extra_data" ]["last_valid_state" ] = lastValidState
324- break
325-
326- # Write modified json
327- with open (RESTORE_STATE_PATH , "w" ) as file :
328- json .dump (restore_state , file , indent = 2 , ensure_ascii = False )
329-
330-
331- def getEntitiesPrecision () -> dict [str : int ]:
332- # Initialize return dictionary
333- returnDict = dict ()
334-
335- # Read file core.config_entries
336- with open (CONFIG_ENTRIES_PATH , "r" ) as file :
337- configEntries = json .load (file )
338-
339- # Read file core.entity_registry
340- with open (ENTITY_REGISTRY_PATH , "r" ) as file :
341- configEntities = json .load (file )
284+ if state ["state" ]["entity_id" ] != entity_id :
285+ continue
342286
343- configIds = dict ()
287+ # Modify state to new value
288+ if state ["state" ].get ("state" , "" ):
289+ state ["state" ]["state" ] = lastValidState
344290
345- # Find entry_ids which have the option/round attribute (these are most likely Riemann Sum Entities)
346- for configEntry in configEntries ["data" ]["entries" ]:
347- number = configEntry ["options" ].get ("round" , - 1 )
348- if number == - 1 :
349- continue
291+ if state ["extra_data" ]:
350292
351- # Store precision value
352- configIds [ configEntry [ "entry_id" ]] = int ( number )
293+ if state [ "extra_data" ]. get ( "last_valid_state" , "" ):
294+ state [ "extra_data" ][ "last_valid_state" ] = lastValidState
353295
354- # Find entity_id for all entry_ids
355- for configEntity in configEntities ["data" ]["entities" ]:
356- if configEntity ["config_entry_id" ] not in configIds :
357- continue
296+ if state ["extra_data" ].get ("native_value" , dict ()).get ("decimal_str" , "" ):
297+ state ["extra_data" ]["native_value" ]["decimal_str" ] = lastValidState
358298
359- entity_id = configEntity ["entity_id" ]
360- config_entry_id = configEntity ["config_entry_id" ]
361- # Store precision and entity_id
362- returnDict [entity_id ] = configIds [config_entry_id ]
299+ break
363300
364- # Return dict with format {entity_id: precision}
365- return returnDict
301+ # Write modified json
302+ with open (RESTORE_STATE_PATH , "w" ) as file :
303+ json .dump (restore_state , file , indent = 2 , ensure_ascii = False )
366304
367305
368306def SqlExec (SqlQuery : str , arguments : tuple ):
0 commit comments