diff --git a/apps/predbat/gecloud.py b/apps/predbat/gecloud.py
index 17fb259b3..c9f2f0138 100644
--- a/apps/predbat/gecloud.py
+++ b/apps/predbat/gecloud.py
@@ -520,6 +520,26 @@ async def publish_meter(self, device, meter):
self.base.dashboard_item(entity_name + "_grid_import_total", state=meter[key][subkey].get("import", 0), attributes=attribute_table.get("grid_import_total", {}), app="gecloud")
self.base.dashboard_item(entity_name + "_grid_export_total", state=meter[key][subkey].get("export", 0), attributes=attribute_table.get("grid_export_total", {}), app="gecloud")
+ async def enable_real_time_control(self, device, registers):
+ for key in registers:
+ reg_name = registers[key].get("name", None)
+ value = registers[key].get("value", None)
+ if "real-time control" in reg_name.lower():
+ if value:
+ self.log("GECloud: Real-time control already enabled for {}".format(device))
+ return True
+ else:
+ self.log("GECloud: Enabling real-time control for {} as current value is {}".format(device, value))
+ result = await self.async_write_inverter_setting(device, key, True)
+ if result and ("value" in result):
+ registers[key]["value"] = result["value"]
+ await self.publish_registers(device, self.settings[device], select_key=key)
+ return True
+ else:
+ self.log("GECloud: Failed to enable real-time control for {}".format(device))
+ return False
+ return False
+
async def publish_registers(self, device, registers, select_key=None):
"""
Publish the registers
@@ -531,7 +551,7 @@ async def publish_registers(self, device, registers, select_key=None):
validation_rules = registers[key].get("validation_rules", None)
validation = registers[key].get("validation", None)
value = registers[key].get("value", None)
- ha_name = reg_name.lower().replace(" ", "_").replace("%", "percent")
+ ha_name = reg_name.lower().replace(" ", "_").replace("%", "percent").replace("-", "_")
attributes = {}
attributes["friendly_name"] = reg_name
@@ -750,6 +770,8 @@ async def start(self):
if seconds == 0 or self.polling_mode or (device == ems_device) or (device == gateway_device):
self.settings[device] = await self.async_get_inverter_settings(device, first=False, previous=self.settings.get(device, {}))
await self.publish_registers(device, self.settings[device])
+ if seconds == 0:
+ await self.enable_real_time_control(device, self.settings[device])
except Exception as e:
self.log("Error: GECloud: Exception in main loop {}".format(e))
diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py
index 32f5df45d..2ddd7eb0b 100644
--- a/apps/predbat/predbat.py
+++ b/apps/predbat/predbat.py
@@ -36,7 +36,7 @@
import requests
import asyncio
-THIS_VERSION = "v8.25.2"
+THIS_VERSION = "v8.25.3"
# fmt: off
PREDBAT_FILES = ["predbat.py", "config.py", "prediction.py", "gecloud.py","utils.py", "inverter.py", "ha.py", "download.py", "unit_test.py", "web.py", "web_helper.py", "predheat.py", "futurerate.py", "octopus.py", "solcast.py","execute.py", "plan.py", "fetch.py", "output.py", "userinterface.py", "energydataservice.py", "alertfeed.py", "compare.py", "db_manager.py", "db_engine.py", "plugin_system.py", "ohme.py", "components.py", "fox.py"]
diff --git a/apps/predbat/web.py b/apps/predbat/web.py
index f1b65a36c..349c7799b 100644
--- a/apps/predbat/web.py
+++ b/apps/predbat/web.py
@@ -1307,6 +1307,12 @@ async def html_log(self, request):
text += ''
text += ""
+ text += '
'
+ text += '
'
+ text += '
'
+ text += '
'
+ text += "
"
+
text += 'Loading log data...
'
text += "\n"
text += "\n"
diff --git a/apps/predbat/web_helper.py b/apps/predbat/web_helper.py
index 84ecc3287..0608f3ac2 100644
--- a/apps/predbat/web_helper.py
+++ b/apps/predbat/web_helper.py
@@ -1804,6 +1804,102 @@ def get_log_css():
background-color: #4CAF50;
color: white;
}
+
+/* Log search container styles */
+.log-search-container {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ padding: 8px;
+ background-color: #f8f9fa;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ gap: 8px;
+}
+
+.log-search-input {
+ flex-grow: 1;
+ padding: 6px 10px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 14px;
+ min-width: 200px;
+}
+
+.log-search-input:focus {
+ outline: none;
+ border-color: #4CAF50;
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
+}
+
+.clear-search-button {
+ padding: 6px 12px;
+ background-color: #6c757d;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ white-space: nowrap;
+}
+
+.clear-search-button:hover {
+ background-color: #5a6268;
+}
+
+.search-status {
+ font-size: 12px;
+ color: #6c757d;
+ white-space: nowrap;
+}
+
+/* Dark mode search styles */
+body.dark-mode .log-search-container {
+ background-color: #2d3748;
+ border-color: #4a5568;
+}
+
+body.dark-mode .log-search-input {
+ background-color: #4a5568;
+ border-color: #718096;
+ color: #fff;
+}
+
+body.dark-mode .log-search-input:focus {
+ border-color: #4CAF50;
+}
+
+body.dark-mode .log-search-input::placeholder {
+ color: #a0aec0;
+}
+
+body.dark-mode .clear-search-button {
+ background-color: #4a5568;
+}
+
+body.dark-mode .clear-search-button:hover {
+ background-color: #2d3748;
+}
+
+body.dark-mode .search-status {
+ color: #a0aec0;
+}
+
+/* Hide filtered log entries */
+.log-entry-hidden {
+ display: none !important;
+}
+
+/* Highlight matching text */
+.search-highlight {
+ background-color: #ffeb3b;
+ font-weight: bold;
+}
+
+body.dark-mode .search-highlight {
+ background-color: #ffa000;
+ color: #000;
+}
"""
@@ -2262,6 +2358,15 @@ def get_logfile_js(filter_type):
}}
}});
+ // Apply current search filter to new entries
+ const searchTerm = document.getElementById('logSearchInput').value.toLowerCase();
+ if (searchTerm.trim() !== '') {{
+ // Re-apply filter to include new entries
+ setTimeout(() => {{
+ filterLogEntries();
+ }}, 10);
+ }}
+
// Auto-scroll to top for new entries (since newest are at top)
if (newEntriesAdded > 0 && shouldAutoScroll) {{
setTimeout(() => {{
@@ -2283,6 +2388,73 @@ def get_logfile_js(filter_type):
}});
}}
+ // Search functionality
+ function filterLogEntries() {{
+ const searchTerm = document.getElementById('logSearchInput').value.toLowerCase();
+ const rows = document.querySelectorAll('#logTableBody tr[data-line]');
+ const statusDiv = document.getElementById('searchStatus');
+
+ let visibleCount = 0;
+ let totalCount = rows.length;
+
+ // If search is empty, show all entries
+ if (searchTerm.trim() === '') {{
+ rows.forEach(row => {{
+ row.classList.remove('log-entry-hidden');
+ // Remove any existing highlights
+ const cells = row.querySelectorAll('td');
+ cells.forEach(cell => {{
+ cell.innerHTML = cell.innerHTML.replace(/(.*?)<\\/mark>/gi, '$1');
+ }});
+ visibleCount++;
+ }});
+ statusDiv.textContent = '';
+ return;
+ }}
+
+ // Filter and highlight entries
+ rows.forEach(row => {{
+ const rowText = row.textContent.toLowerCase();
+
+ if (rowText.includes(searchTerm)) {{
+ row.classList.remove('log-entry-hidden');
+ visibleCount++;
+
+ // Highlight matching text
+ const cells = row.querySelectorAll('td');
+ cells.forEach(cell => {{
+ // First remove any existing highlights
+ cell.innerHTML = cell.innerHTML.replace(/(.*?)<\\/mark>/gi, '$1');
+
+ // Then add new highlights
+ const regex = new RegExp(`(${{escapeRegExp(searchTerm)}})`, 'gi');
+ cell.innerHTML = cell.innerHTML.replace(regex, '$1');
+ }});
+ }} else {{
+ row.classList.add('log-entry-hidden');
+ // Remove highlights from hidden rows
+ const cells = row.querySelectorAll('td');
+ cells.forEach(cell => {{
+ cell.innerHTML = cell.innerHTML.replace(/(.*?)<\\/mark>/gi, '$1');
+ }});
+ }}
+ }});
+
+ // Update status
+ statusDiv.textContent = `Showing ${{visibleCount}} of ${{totalCount}} entries`;
+ }}
+
+ // Clear search function
+ function clearLogSearch() {{
+ document.getElementById('logSearchInput').value = '';
+ filterLogEntries();
+ }}
+
+ // Escape special regex characters
+ function escapeRegExp(string) {{
+ return string.replace(/[.*+?^${{}}()|[\\]\\\\]/g, '\\\\$&');
+ }}
+
// Fetch new log entries
async function updateLog() {{
if (isUpdating || isPaused) return;