Skip to content

Commit 9cd70be

Browse files
Add db manager and ha to components
1 parent 1534a68 commit 9cd70be

6 files changed

Lines changed: 116 additions & 56 deletions

File tree

‎apps/predbat/components.py‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,37 @@
1313
from ohme import OhmeAPI
1414
from octopus import OctopusAPI
1515
from web import WebInterface
16+
from ha import HAInterface
17+
from db_manager import DatabaseManager
18+
import os
1619

1720
COMPONENT_LIST = {
18-
"web": {"class": WebInterface, "name": "Web Interface", "args": {"port": {"required": False, "config": "web_port", "default": 5052}}},
21+
"db": {
22+
"class": DatabaseManager,
23+
"name": "Database Manager",
24+
"args": {
25+
"db_enable": {"required": True, "config": "db_enable"},
26+
"db_days": {"required": False, "config": "db_days", "default": 30},
27+
}
28+
},
29+
"ha": {
30+
"class": HAInterface,
31+
"name": "Home Assistant Interface",
32+
"args": {
33+
"ha_url": {"required": False, "config": "ha_url", "default": "http://supervisor/core"},
34+
"ha_key": {"required": False, "config": "ha_key", "default": os.environ.get("SUPERVISOR_TOKEN", None)},
35+
"db_enable": {"required": False, "config": "db_enable", "default": False},
36+
"db_mirror_ha": {"required": False, "config": "db_mirror_ha", "default": False},
37+
"db_primary": {"required": False, "config": "db_primary", "default": False},
38+
}
39+
},
40+
"web": {
41+
"class": WebInterface,
42+
"name": "Web Interface",
43+
"args": {
44+
"port": {"required": False, "config": "web_port", "default": 5052}
45+
}
46+
},
1947
"gecloud": {
2048
"class": GECloudDirect,
2149
"name": "GivEnergy Cloud Direct",
@@ -101,9 +129,11 @@ def start(self):
101129
raise ValueError(f"{component_info['name']} API failed to start")
102130

103131
async def stop(self):
104-
for component_name, component in self.components.items():
132+
for component_name, component_info in reversed(list(self.components.items())):
133+
component = self.components[component_name]
105134
if component:
106135
self.log(f"Stopping {component_name} interface")
136+
print(component_name, component)
107137
await component.stop()
108138
self.component_tasks[component_name] = None
109139
self.components[component_name] = None
@@ -114,17 +144,20 @@ async def stop(self):
114144

115145
async def select_event(self, entity_id, value):
116146
for component_name, component in self.components.items():
117-
if component and COMPONENT_LIST[component_name].get("event_filter", "") in entity_id:
147+
event_filter = COMPONENT_LIST[component_name].get("event_filter", None)
148+
if component and event_filter and (event_filter in entity_id):
118149
await component.select_event(entity_id, value)
119150

120151
async def switch_event(self, entity_id, service):
121152
for component_name, component in self.components.items():
122-
if component and COMPONENT_LIST[component_name].get("event_filter", "") in entity_id:
153+
event_filter = COMPONENT_LIST[component_name].get("event_filter", None)
154+
if component and event_filter and (event_filter in entity_id):
123155
await component.switch_event(entity_id, service)
124156

125157
async def number_event(self, entity_id, value):
126158
for component_name, component in self.components.items():
127-
if component and COMPONENT_LIST[component_name].get("event_filter", "") in entity_id:
159+
event_filter = COMPONENT_LIST[component_name].get("event_filter", None)
160+
if component and event_filter and (event_filter in entity_id):
128161
await component.number_event(entity_id, value)
129162

130163
def is_all_alive(self):
@@ -135,9 +168,14 @@ def is_alive(self, name):
135168
"""Check if a single component is alive"""
136169
if name not in self.components:
137170
return False
171+
if not self.components[name]:
172+
# Disabled components can be ignored
173+
return True
138174
if not self.component_tasks[name] or not self.component_tasks[name].is_alive():
139175
return False
140-
return self.components[name].is_alive()
176+
if not self.components[name].is_alive():
177+
return False
178+
return True
141179

142180
def get_active(self):
143181
active_components = [name for name, comp in self.components.items() if comp]

‎apps/predbat/db_manager.py‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717

1818
class DatabaseManager:
19-
def __init__(self, base, db_days):
19+
def __init__(self, db_enable, db_days, base):
2020
self.base = base
2121
self.log = base.log
2222
self.db_days = db_days
@@ -27,6 +27,7 @@ def __init__(self, base, db_days):
2727
self.sync_event = threading.Event()
2828
self.async_event = asyncio.Event()
2929
self.return_event = threading.Event()
30+
self.api_started = False
3031

3132
def bridge_event(self, loop):
3233
"""
@@ -39,6 +40,24 @@ def bridge_event(self, loop):
3940
self.sync_event.clear() # Clear the event to allow the loop to continue
4041
loop.call_soon_threadsafe(self.async_event.set)
4142

43+
def is_alive(self):
44+
"""Check if the database manager is alive"""
45+
return self.api_started
46+
47+
def wait_api_started(self):
48+
"""
49+
Wait for the API to start
50+
"""
51+
self.log("DBManager: Waiting for API to start")
52+
count = 0
53+
while not self.api_started and count < 240:
54+
time.sleep(1)
55+
count += 1
56+
if not self.api_started:
57+
self.log("Warn: DBManager: Failed to start")
58+
return False
59+
return True
60+
4261
async def start(self):
4362
"""
4463
Initialize the database and clean up old data
@@ -52,6 +71,7 @@ async def start(self):
5271
threading.Thread(target=self.bridge_event, args=(loop,), daemon=True).start()
5372

5473
self.log("db_manager: Started")
74+
self.api_started = True
5575

5676
while not self.stop_thread:
5777
if not self.db_queue:
@@ -87,6 +107,7 @@ async def start(self):
87107

88108
self.db_engine._close()
89109
self.log("db_manager: Stopped")
110+
self.api_started = False
90111

91112
def send_via_ipc(self, command, info, expect_response=False):
92113
"""
@@ -114,14 +135,14 @@ def send_via_ipc(self, command, info, expect_response=False):
114135
return None
115136
return None
116137

117-
def stop(self):
138+
async def stop(self):
118139
"""
119140
Close the database connection
120141
"""
121142
self.stop_thread = True
122-
queue_id = self.queue_id
123-
self.queue_id += 1
124143
self.send_via_ipc("stop", {}, expect_response=False)
144+
self.api_started = False
145+
self.log("db_manager: stop command sent")
125146

126147
def get_state_db(self, entity_id):
127148
"""

‎apps/predbat/ha.py‎

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import requests
1818
import traceback
1919
import threading
20+
import time
2021
from config import TIME_FORMAT_HA, TIMEOUT, TIME_FORMAT_HA_TZ
2122

2223
from db_manager import DatabaseManager # database functions have been refactored to DatabaseManager
@@ -50,37 +51,51 @@ class HAInterface:
5051
"""
5152
Direct interface to Home Assistant
5253
"""
54+
def is_active(self):
55+
return self.is_alive()
5356

54-
def is_running(self):
57+
def is_alive(self):
58+
if not self.api_started:
59+
return False
5560
if self.websocket_task:
5661
if not self.websocket_task.is_alive():
5762
return False
5863
if not self.websocket_active:
5964
return False
60-
if self.db_manager_task:
61-
if not self.db_manager_task.is_alive():
62-
return False
6365
return True
6466

65-
def __init__(self, base):
67+
def wait_api_started(self):
68+
"""
69+
Wait for the API to start
70+
"""
71+
self.log("HAInterface: Waiting for API to start")
72+
count = 0
73+
while not self.api_started and count < 240:
74+
time.sleep(1)
75+
count += 1
76+
if not self.api_started:
77+
self.log("Warn: HAInterface: Failed to start")
78+
return False
79+
return True
80+
81+
def __init__(self, ha_url, ha_key, db_enable, db_mirror_ha, db_primary, base):
6682
"""
6783
Initialize the interface to Home Assistant.
6884
"""
69-
self.ha_url = base.args.get("ha_url", "http://supervisor/core")
70-
self.ha_key = base.args.get("ha_key", os.environ.get("SUPERVISOR_TOKEN", None))
71-
self.db_enable = base.args.get("db_enable", False)
72-
self.db_days = base.args.get("db_days", 30)
73-
self.db_mirror_ha = base.args.get("db_mirror_ha", False)
74-
self.db_primary = base.args.get("db_primary", False)
85+
self.ha_url = ha_url
86+
self.ha_key = ha_key
87+
self.db_enable = db_enable
88+
self.db_mirror_ha = db_mirror_ha
89+
self.db_primary = db_primary
7590
self.db_manager = None
76-
self.db_manager_task = None
7791
self.db_mirror_list = {}
7892
self.db = None
7993
self.db_cursor = None
8094
self.websocket_active = False
8195
self.websocket_task = None
8296
self.api_errors = 0
8397
self.stop_thread = False
98+
self.api_started = False
8499

85100
self.base = base
86101
self.log = base.log
@@ -93,43 +108,39 @@ def __init__(self, base):
93108
raise ValueError
94109
else:
95110
self.log("Info: Using SQL Lite database as primary data source, no HA interface available")
96-
else:
97-
self.log("Getting slug id from HA supervisor")
98111

112+
if self.ha_key:
99113
# Get the current addon info, but suppress warning message if the API call fails as non-HAOS installs won't have supervisor running
100114
res = self.api_call("/addons/self/info", core=False, silent=True)
101115
if res:
102116
# get add-on slug name which is the actual directory name under /addon_configs that /config is mounted to
103117
self.slug = res["data"]["slug"]
104118
self.log("Info: Add-on slug is {}".format(self.slug))
105119

106-
self.log("Checking Predbat can talk to HA")
107-
108120
check = self.api_call("/api/services")
109121
if not check:
110122
self.log("Warn: Unable to connect directly to Home Assistant at {}, please check your configuration of ha_url/ha_key".format(self.ha_url))
111123
self.ha_key = None
112124
raise ValueError
113-
else:
114-
self.log("Info: Connected to Home Assistant at {}".format(self.ha_url))
115-
116-
self.log("Creating websocket")
117-
118-
self.websocket_task = self.base.create_task(self.socketLoop())
119-
self.websocket_active = True
120-
self.log("Info: Web Socket task started")
121125

122126
if self.db_enable:
123127
# Open the SQL Lite database called predbat.db using the DatabaseManager
124-
self.log("Info: Opening database")
125-
self.db_manager = DatabaseManager(self.base, self.db_days)
126-
self.db_manager_task = self.base.create_task(self.db_manager.start())
128+
self.db_manager = self.base.components.get_component("db")
129+
130+
# API Has started
131+
self.base.ha_interface = self # Set pointer back to ourselves as other components require this one
132+
self.api_started = True
133+
134+
async def start(self):
135+
""" Async start not required
136+
"""
137+
self.log("Info: Starting HA interface")
138+
self.websocket_active = True
139+
await self.socketLoop()
127140

128-
def stop(self):
141+
async def stop(self):
129142
self.stop_thread = True
130-
if self.db_manager:
131-
self.log("Info: Stopping database manager")
132-
self.db_manager.stop()
143+
self.api_started = False
133144

134145
def get_slug(self):
135146
"""

‎apps/predbat/octopus.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ async def save_octopus_cache(self):
416416
with open(self.cache_file, "w") as f:
417417
yaml.dump(octopus_cache, f)
418418

419-
def stop(self):
419+
async def stop(self):
420420
self.stop_api = True
421421

422422
def get_tariff(self, tariff_type):

‎apps/predbat/predbat.py‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@
7070
)
7171
from prediction import reset_prediction_globals
7272
from utils import minutes_since_yesterday, dp1, dp2, dp3
73-
from ha import HAInterface
7473
from predheat import PredHeat
7574
from octopus import Octopus
7675
from energydataservice import Energidataservice
@@ -1278,8 +1277,6 @@ def is_running(self):
12781277

12791278
if not self.ha_interface:
12801279
return False
1281-
if not self.ha_interface.is_running():
1282-
return False
12831280

12841281
if self.components:
12851282
if not self.components.is_all_alive():
@@ -1318,19 +1315,14 @@ def initialize(self):
13181315
try:
13191316
self.reset()
13201317
self.update_time(print=False)
1321-
self.log("Starting HA interface")
1322-
try:
1323-
self.ha_interface = HAInterface(self)
1324-
except ValueError as e:
1325-
self.log("Error: Exception raised {}".format(e))
1326-
self.log("Error: " + traceback.format_exc())
1327-
self.record_status("Error: Exception raised {}".format(e), debug=traceback.format_exc())
1328-
raise e
13291318

13301319
# Start all sub-components
13311320
self.components = Components(self)
13321321
self.components.start()
13331322

1323+
if not self.ha_interface:
1324+
raise ValueError("HA interface not found")
1325+
13341326
# Initialize plugin system and discover plugins
13351327
self.init_plugin_system()
13361328

@@ -1396,8 +1388,6 @@ async def terminate(self):
13961388
self.stop_thread = True
13971389
if self.components:
13981390
await self.components.stop()
1399-
if self.ha_interface:
1400-
self.ha_interface.stop()
14011391

14021392
await asyncio.sleep(0)
14031393
if hasattr(self, "pool"):

‎apps/predbat/web.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ def get_status_html(self, status, version):
472472
if status and (("Warn:" in status) or ("Error:" in status)):
473473
text += "<tr><td>Status</td><td bgcolor=#ff7777>{}</td></tr>\n".format(status)
474474
elif not is_running:
475-
text += "<tr><td colspan='2' bgcolor='#ff7777'>Predbat has errors</td></tr>\n"
475+
text += "<tr><td colspan='2' bgcolor='#ff7777'>{} (with errors)</td></tr>\n".format(status)
476476
else:
477477
text += "<tr><td>Status</td><td>{}</td></tr>\n".format(status)
478478
text += "<tr><td>Last Updated</td><td>{}</td></tr>\n".format(last_updated)

0 commit comments

Comments
 (0)