diff --git a/apps/predbat/gecloud.py b/apps/predbat/gecloud.py index e7f8d5c55..1ac7efd79 100644 --- a/apps/predbat/gecloud.py +++ b/apps/predbat/gecloud.py @@ -670,6 +670,7 @@ async def async_automatic_config(self, devices): self.base.args["charge_limit"] = ["number.predbat_gecloud_" + device + "_ac_charge_upper_percent_limit" for device in batteries] self.base.args["discharge_start_time"] = ["select.predbat_gecloud_" + device + "_dc_discharge_1_start_time" for device in batteries] self.base.args["discharge_end_time"] = ["select.predbat_gecloud_" + device + "_dc_discharge_1_end_time" for device in batteries] + self.base.args["discharge_target_soc"] = ["select.predbat_gecloud_" + device + "_dc_discharge_1_lower_soc_percent_limit" for device in batteries] self.base.args["scheduled_charge_enable"] = ["switch.predbat_gecloud_" + device + "_ac_charge_enable" for device in batteries] self.base.args["scheduled_discharge_enable"] = ["switch.predbat_gecloud_" + device + "_enable_dc_discharge" for device in batteries] self.base.args["pause_mode"] = ["select.predbat_gecloud_" + device + "_pause_battery" for device in batteries] diff --git a/apps/predbat/ha.py b/apps/predbat/ha.py index c15af66fa..276a155d7 100644 --- a/apps/predbat/ha.py +++ b/apps/predbat/ha.py @@ -465,7 +465,6 @@ async def set_state_external(self, entity_id, state, attributes={}): new_state = {"entity_id": entity_id, "state": state, "attributes": attributes} old_value = self.get_state(entity_id) old_state = self.state_data.get(entity_id.lower(), None) - found = False # See if the modified entity is in the CONFIG_ITEMS list for item in self.base.CONFIG_ITEMS: @@ -495,13 +494,31 @@ async def set_state_external(self, entity_id, state, attributes={}): await self.base.trigger_callback(service_data) return - if not found: - # If its not found then its a sensor and we can set the state directly - self.set_state(entity_id, state, attributes) + # Call back to Predbat handled entities + domain = entity_id.split(".")[0] if "." in entity_id else "" + service_data = {"domain": domain} + if domain in ["switch", "input_boolean"]: + # For toggle switches, check if value is 'on' or 'off' + service_data["service"] = "turn_on" if new_value else "turn_off" + service_data["service_data"] = {"entity_id": entity_id} + elif domain in ["number", "input_number"]: + service_data["service"] = "set_value" + service_data["service_data"] = {"entity_id": entity_id, "value": new_value} + elif domain in ["select", "input_select"]: + service_data["service"] = "select_option" + service_data["service_data"] = {"entity_id": entity_id, "option": new_value} + else: + service_data = None + if service_data: + await self.base.trigger_callback(service_data) + return + + # If its not found then its a sensor and we can set the state directly + self.set_state(entity_id, state, attributes) - # Only trigger on value change or you get too many updates - if (old_value is None) or (new_value != old_value): - await self.trigger_watch_list(entity_id, attributes, old_state, new_state) + # Only trigger on value change or you get too many updates + if (old_value is None) or (new_value != old_value): + await self.base.trigger_watch_list(entity_id, attributes, old_state, new_state) def set_state(self, entity_id, state, attributes={}): """ diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index c954949bf..447357f81 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -1110,8 +1110,8 @@ def calculate_plan(self, recompute=True, debug_mode=False, publish=True): self.publish_html_plan(pv_forecast_minute_step, pv_forecast_minute10_step, load_minutes_step, load_minutes_step10, self.end_record) # Web history - if self.components.get_component("web_interface"): - self.components.get_component("web_interface").history_update() + if self.components.get_component("web"): + self.components.get_component("web").history_update() # Destroy pool if self.pool: diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 349c7799b..61ebe75b0 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -122,6 +122,7 @@ async def start(self): app.router.add_get("/charts", self.html_charts) app.router.add_get("/config", self.html_config) app.router.add_get("/entity", self.html_entity) + app.router.add_post("/entity", self.html_entity_post) app.router.add_post("/config", self.html_config_post) app.router.add_get("/dash", self.html_dash) app.router.add_post("/dash", self.html_dash_post) @@ -694,6 +695,10 @@ async def html_entity(self, request): else: text += config_text + control_html = self.get_entity_control_html(entity, days) + if control_html: + text += control_html + text += "

History Chart

\n" text += '
' now_str = self.base.now_utc.strftime(TIME_FORMAT) @@ -736,6 +741,186 @@ async def html_entity(self, request): text += "\n" return web.Response(content_type="text/html", text=text) + async def html_entity_post(self, request): + """ + Handle POST request for entity value updates + """ + try: + postdata = await request.post() + entity_id = postdata.get("entity_id", "") + new_value = postdata.get("value", "") + days = postdata.get("days", "7") + attributes = self.base.dashboard_values.get(entity_id, {}).get("attributes", {}) + + if entity_id: + # Determine entity type from entity_id domain + domain = entity_id.split(".")[0] if "." in entity_id else "" + + # Convert value based on entity type + if domain in ["switch", "input_boolean"]: + # For toggle switches, check if value is 'on' or 'off' + new_value = new_value.lower() == "on" + elif domain in ["number", "input_number"]: + try: + new_value = float(new_value) if new_value else 0.0 + except ValueError: + new_value = 0.0 + elif domain in ["select", "input_select"]: + # Keep as string for select entities + pass + + # Set the entity state + await self.base.ha_interface.set_state_external(entity_id, new_value, attributes=attributes) + self.log(f"Entity {entity_id} updated to {new_value} via web interface") + + except Exception as e: + self.log(f"Error updating entity value: {e}") + + # Redirect back to entity page with same parameters + redirect_url = f"./entity?entity_id={entity_id}&days={days}" + raise web.HTTPFound(redirect_url) + + def is_entity_writable(self, entity_id): + """ + Determine if an entity is writable and return its control type + Returns: (is_writable, control_type, options) + """ + if not entity_id or "." not in entity_id: + return False, None, None + + domain = entity_id.split(".")[0] + + # Check if it's a writable entity type + if domain in ["switch", "input_boolean"]: + return True, "toggle", None + elif domain in ["number", "input_number"]: + return True, "number", None + elif domain in ["select", "input_select"]: + options = self.base.get_state_wrapper(entity_id=entity_id, attribute="options", default=[]) + return True, "select", options + + return False, None, None + + def get_entity_control_html(self, entity_id, days): + """ + Generate HTML control for writable entities + """ + current_value = self.base.get_state_wrapper(entity_id=entity_id) + + is_writable, control_type, options = self.is_entity_writable(entity_id) + + if not is_writable: + return "" + + html = '
' + + # Add CSS for dark mode support + html += """ + + """ + html += f'
' + html += f'' + html += f'' + + if control_type == "toggle": + is_active = str(current_value).lower() in ["true", "on", "1"] + toggle_class = "toggle-switch active" if is_active else "toggle-switch" + + html += f'
' + html += f'' + html += f"Enable/Disable" + html += f"
" + + # Add JavaScript for toggle functionality + html += f""" + + """ + + elif control_type == "number": + min_val = self.base.get_state_wrapper(entity_id=entity_id, attribute="min", default=None) + max_val = self.base.get_state_wrapper(entity_id=entity_id, attribute="max", default=None) + step = self.base.get_state_wrapper(entity_id=entity_id, attribute="step", default=1) + unit = self.base.get_state_wrapper(entity_id=entity_id, attribute="unit_of_measurement", default="") + + unit_display = f" {unit}" if unit else "" + + html += f'{unit}' + html += '' + + elif control_type == "select" and options: + html += f'" + html += '' + + html += "
" + html += "
" + + return html + def get_entity_detailedForecast(self, entity, subitem="pv_estimate"): results = {} detailedForecast = self.base.dashboard_values.get(entity, {}).get("attributes", {}).get("detailedForecast", {})