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;