Skip to content

Commit 82a8eee

Browse files
committed
Update to version 2.1.1
1 parent 8cf7c11 commit 82a8eee

2 files changed

Lines changed: 54 additions & 111 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG.md
22

3+
## 2.1.1
4+
* Fixed a bug where --list did not list all entities with has_sum=1
5+
* Added *autocommit=True* for MySQL server connection
6+
* No longer round values but handle all values as they are stored in the database
7+
38
## 2.1.0
49
* Add support for a MySQL server using the package PyMySQL
510
* Add support for entities that are no Riemann Sum Entities

HA_FixNegativeStatistics.py

Lines changed: 49 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
import sys
1313
import shutil
1414
import sqlite3
15+
from decimal import Decimal, InvalidOperation
1516
from datetime import datetime
1617

1718
__author__ = "Sebastian Hollas"
18-
__version__ = "2.1.0"
19+
__version__ = "2.1.1"
1920

2021
####################################################################################
2122
# USER INPUT REQUIRED !
@@ -35,27 +36,19 @@
3536
# Build Filepaths
3637
ENTITIES_FILE = os.path.join(HA_CONFIG_ROOT, "entities.list")
3738
RESTORE_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

4140
if 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
5244
if 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"\nPlease adjust to your needs and rerun the script with no arguments.")
@@ -119,8 +115,6 @@ def main():
119115

120116

121117
def 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

236211
def 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

368306
def SqlExec(SqlQuery: str, arguments: tuple):

0 commit comments

Comments
 (0)