From 2f9068517ec3f80dd46cf2e11d9715112c2e8706 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:11:49 +0000 Subject: [PATCH 1/8] Validate config --- apps/predbat/config.py | 105 ++++++++++++++++++++++++++++ apps/predbat/gecloud.py | 3 +- apps/predbat/predbat.py | 148 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index 2cad24f1..46049012 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1456,3 +1456,108 @@ "Feed-in priority - No Timed Charge/Discharge": 96, "Feed-in priority": 98, } + +# Apps.yaml validation schema +APPS_SCHEMA = { + "currency_symbols": {"type": "string|string_list"}, + "db_enable" : {"type": "boolean"}, + "db_days" : {"type": "integer"}, + "db_mirror_ha" : {"type": "boolean"}, + "db_primary" : {"type": "boolean"}, + "threads" : { + "type": "string", + "allowed": ["auto", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] + }, + "ha_url" : {"type": "string", "empty": False}, + "ha_key" : {"type": "string", "empty": False}, + "load_filter_threshold" : {"type": "integer"}, + "web_port" : {"type": "integer"}, + "load_today" : { + "type": "sensor|sensor_list", + "sensor_type": "float", + "required": True + }, + "import_today" : { + "type": "sensor|sensor_list", + "sensor_type": "float", + }, + "export_today" : { + "type": "sensor|sensor_list", + "sensor_type": "float", + }, + "pv_today" : { + "type": "sensor|sensor_list", + "sensor_type": "float", + }, + "load_forecast_only" : { + "type": "boolean" + }, + "load_forecast" : { + "type": "sensor|sensor_list", + "sensor_type": "float", + }, + "ge_cloud_data" : {"type": "boolean"}, + "ge_cloud_serial" : {"type": "string", "empty": False}, + "ge_cloud_key" : {"type": "string", "empty": False}, + "ge_cloud_direct" : {"type": "boolean"}, + "ge_cloud_automatic" : {"type": "boolean"}, + "num_inverters" : {"type": "integer", "zero": False}, + "balance_inverters_seconds" : {"type": "integer", "zero": False}, + "givtcp_rest" : {"type": "string_list"}, + "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, + "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, + "battery_power": {"type": "sensor_list", "sensor_type": "float"}, + "pv_power": {"type": "sensor_list", "sensor_type": "float"}, + "load_power": {"type": "sensor_list", "sensor_type": "float"}, + "soc_kw": {"type": "sensor_list", "sensor_type": "float"}, + "soc_max": {"type": "sensor_list", "sensor_type": "float"}, + "reserve": {"type": "sensor_list", "sensor_type": "float"}, + "inverter_mode": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "inverter_time": {"type": "sensor_list", "sensor_type": "string"}, + "charge_start_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "charge_end_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "charge_limit": {"type": "sensor_list", "sensor_type": "float", "modify": True}, + "scheduled_charge_enable": {"type": "sensor_list", "sensor_type": "switch", "modify": True}, + "scheduled_discharge_enable": {"type": "sensor_list", "sensor_type": "switch", "modify": True}, + "discharge_start_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "discharge_end_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "battery_temperature": {"type": "sensor_list", "sensor_type": "float"}, + "pause_mode": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "pause_start_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "pause_end_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, + "inverter_limit": {"type": "integer_list", "zero": False}, + "inverter_limit_charge": {"type": "integer_list", "zero": False}, + "export_limit": {"type": "integer_list"}, + "inverter_battery_rate_min": {"type": "integer", "zero": False}, + "inverter_reserve_max": {"type": "integer", "zero": False}, + "battery_charge_power_curve: ": {"type": "int_float_dict"}, + "battery_discharge_power_curve: ": {"type": "int_float_dict"}, + "clock_skew": {"type": "integer"}, + "solcast_api_key": {"type": "string", "empty": False}, + "solcast_host": {"type": "string", "empty": False}, + "solcast_poll_hours": {"type": "float", "zero": False}, + "pv_forecast_today": {"type": "sensor", "sensor_type": "float"}, + "pv_forecast_tomorrow": {"type": "sensor", "sensor_type": "float"}, + "pv_forecast_d3": {"type": "sensor", "sensor_type": "float"}, + "pv_forecast_d4": {"type": "sensor", "sensor_type": "float"}, + "car_charging_energy": {"type": "sensor", "sensor_type": "float"}, + "num_cars": {"type": "integer", "zero": True}, + "car_charging_planned": {"type": "sensor|sensor_list", "sensor_type": "string"}, + "car_charging_planned_response": {"type": "string_list"}, + "car_charging_now": {"type": "sensor|sensor_list", "sensor_type": "string"}, + "car_charging_now_response": {"type": "string_list"}, + "car_charging_battery_size": {"type": "integer|sensor", "zero": False}, + "car_charging_soc": {"type": "sensor|integer"}, + "car_charging_limit": {"type": "integer|sensor"}, + "car_charging_exclusive": {"type": "boolean_list"}, + "carbon_intensity": {"type": "sensor", "sensor_type": "float"}, + "octopus_intelligent_slot": {"type": "sensor", "sensor_type": "switch"}, + "octopus_ready_time": {"type": "sensor", "sensor_type": "string"}, + "octopus_charge_limit": {"type": "sensor", "sensor_type": "float"}, + "octopus_slot_low_rate": {"type": "boolean"}, + "octopus_saving_session_octopoints_per_penny": {"type": "integer"}, + "octopus_free_url": {"type": "string", "empty": False}, +} + + + diff --git a/apps/predbat/gecloud.py b/apps/predbat/gecloud.py index 51eb2d4f..60ee85c7 100644 --- a/apps/predbat/gecloud.py +++ b/apps/predbat/gecloud.py @@ -564,7 +564,8 @@ async def async_automatic_config(self, devices): self.base.args["pause_mode"] = ["select.predbat_gecloud_" + device + "_pause_battery" for device in batteries] self.base.args["pause_start_time"] = ["select.predbat_gecloud_" + device + "_pause_battery_start_time" for device in batteries] self.base.args["pause_end_time"] = ["select.predbat_gecloud_" + device + "_pause_battery_end_time" for device in batteries] - self.base.args["givtcp_rest"] = {} + if 'givtcp_rest' in self.base.args: + del self.base.args['givtcp_rest'] self.base.args["ge_cloud_serial"] = batteries[0] # reconfigure for EMS diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index fd282129..02ce7dbd 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -71,6 +71,7 @@ CONFIG_ROOTS, CONFIG_REFRESH_PERIOD, CONFIG_ITEMS, + APPS_SCHEMA, ) from prediction import reset_prediction_globals from utils import minutes_since_yesterday, dp1, dp2, dp3, dp4 @@ -225,6 +226,11 @@ def get_state_wrapper(self, entity_id=None, default=None, attribute=None, refres if not self.ha_interface: self.log("Error: get_state_wrapper - No HA interface available") return None + + # Entity with coded attribute + if entity_id and '$' in entity_id: + entity_id, attribute = entity_id.split('$') + return self.ha_interface.get_state(entity_id=entity_id, default=default, attribute=attribute, refresh=refresh) def set_state_wrapper(self, entity_id, state, attributes={}): @@ -864,6 +870,145 @@ def download_predbat_version(self, version): self.log("Warn: Predbat update failed to download Predbat version {}".format(version)) return False + def validate_config(self): + """ + Uses APPS_SCHEMA to validate the self.args configuration read from apps.yaml + """ + errors = 0 + for name in APPS_SCHEMA: + spec = APPS_SCHEMA[name] + required = spec.get("required", False) + expected_type = spec.get("type", "string") + + # Check required + if required and name not in self.args: + self.log("Warn: Validation of apps.yaml found missing configuration item '{}'".format(name)) + + # Check type + if name in self.args: + value = self.get_arg(name, indirect=False) + expected_types = expected_type.split("|") + allowed = spec.get("allowed", None) + matches = False + for expected_type in expected_types: + if expected_type == "integer" or expected_type == "integer_list": + if expected_type == "integer" and isinstance(value, int): + value = [value] + if isinstance(value, list): + matches = True + for item in value: + if not isinstance(item, int): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not an integer".format(name, item)) + errors += 1 + break + if spec.get("zero", False) and value == 0: + self.log("Warn: Validation of apps.yaml found configuration item '{}' is zero".format(name)) + errors += 1 + break + elif expected_type == "float": + matches = isinstance(value, float) + elif expected_type == "string": + matches = isinstance(value, str) + if matches and spec.get("empty", False) and not value: + self.log("Warn: Validation of apps.yaml found configuration item '{}' is empty".format(name)) + errors += 1 + break + elif expected_type == "boolean" or expected_type == "boolean_list": + if expected_type == "boolean" and isinstance(value, bool): + value = [value] + if isinstance(value, list): + matches = True + for item in value: + if not isinstance(item, bool): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a boolean".format(name, item)) + errors += 1 + break + elif expected_type == "list": + matches = isinstance(value, list) + elif expected_type == "int_float_dict": + if instance(value, dict): + matches = True + for key in value: + if not isinstance(key, int): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} value {} is not a int => float".format(name, key, value)) + errors += 1 + break + if not isinstance(value[key], float): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} value {} is not a int => float".format(name, key, value)) + errors += 1 + break + elif expected_type == "string_list": + if isinstance(value, list): + matches = True + for item in value: + if not isinstance(item, str): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a string".format(name, item)) + errors += 1 + break + elif expected_type == "sensor_list" or expected_type == "sensor": + if expected_type == "sensor" and isinstance(value, str): + value = [value] + if isinstance(value, list): + matches = True + for sensor in value: + if not isinstance(sensor, str): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a string".format(name, sensor)) + errors += 1 + break + if '.' not in sensor: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id".format(name, sensor)) + errors += 1 + break + if spec.get("modify", False): + prefix = sensor.split(".")[0] + if prefix not in ['switch', 'select', 'input_number', 'number']: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} can not be modified".format(name, sensor)) + errors += 1 + break + + state = self.get_state_wrapper(sensor) + sensor_type = spec.get("sensor_type", None) + if state is None: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} returned value None".format(name, sensor)) + errors += 1 + break + if sensor_type and sensor_type == "float": + try: + float(state) + except ValueError: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a float".format(name, sensor)) + errors += 1 + break + elif sensor_type and sensor_type == "integer": + try: + int(state) + except ValueError: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not an integer".format(name, sensor)) + errors += 1 + break + elif sensor_type and sensor_type == "switch": + if state not in ["on", "off", True, False]: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a switch".format(name, sensor)) + errors += 1 + break + + if matches: + break + if not matches: + self.log("Warn: Validation of apps.yaml found configuration item '{}' is not of type '{}' value was {}".format(name, expected_type, value)) + errors += 1 + elif allowed: + if value not in allowed: + self.log("Warn: Validation of apps.yaml found configuration item '{}' value {} is not in allowed list {}".format(name, value, allowed)) + errors += 1 + if errors: + self.log("Error: Validation of apps.yaml found {} configuration errors".format(errors)) + self.record_status("Error: Validation of apps.yaml found {} configuration errors".format(errors)) + else: + self.log("Validation of apps.yaml completed with no errors") + + return errors + def initialize(self): """ Setup the app, called once each time the app starts @@ -924,6 +1069,7 @@ def initialize(self): self.ha_interface.update_states() self.auto_config() self.load_user_config(quiet=False, register=True) + self.validate_config() self.comparison = Compare(self) except Exception as e: self.log("Error: Exception raised {}".format(e)) @@ -1007,6 +1153,7 @@ def update_time_loop(self, cb_args): self.prediction_started = True self.ha_interface.update_states() self.load_user_config() + self.validate_config() self.update_pending = False try: self.update_pred(scheduled=False) @@ -1064,6 +1211,7 @@ def run_time_loop(self, cb_args): if self.update_pending: self.load_user_config() + self.validate_config() config_changed = True self.update_pending = False From b073d1a30058a8902badc6c395d10cd537f415bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:13:09 +0000 Subject: [PATCH 2/8] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/config.py | 58 ++++++++++++++++------------------------- apps/predbat/gecloud.py | 4 +-- apps/predbat/predbat.py | 8 +++--- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index 46049012..35259407 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1460,50 +1460,41 @@ # Apps.yaml validation schema APPS_SCHEMA = { "currency_symbols": {"type": "string|string_list"}, - "db_enable" : {"type": "boolean"}, - "db_days" : {"type": "integer"}, - "db_mirror_ha" : {"type": "boolean"}, - "db_primary" : {"type": "boolean"}, - "threads" : { - "type": "string", - "allowed": ["auto", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] - }, - "ha_url" : {"type": "string", "empty": False}, - "ha_key" : {"type": "string", "empty": False}, - "load_filter_threshold" : {"type": "integer"}, - "web_port" : {"type": "integer"}, - "load_today" : { + "db_enable": {"type": "boolean"}, + "db_days": {"type": "integer"}, + "db_mirror_ha": {"type": "boolean"}, + "db_primary": {"type": "boolean"}, + "threads": {"type": "string", "allowed": ["auto", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}, + "ha_url": {"type": "string", "empty": False}, + "ha_key": {"type": "string", "empty": False}, + "load_filter_threshold": {"type": "integer"}, + "web_port": {"type": "integer"}, + "load_today": {"type": "sensor|sensor_list", "sensor_type": "float", "required": True}, + "import_today": { "type": "sensor|sensor_list", "sensor_type": "float", - "required": True }, - "import_today" : { + "export_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "export_today" : { + "pv_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "pv_today" : { + "load_forecast_only": {"type": "boolean"}, + "load_forecast": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "load_forecast_only" : { - "type": "boolean" - }, - "load_forecast" : { - "type": "sensor|sensor_list", - "sensor_type": "float", - }, - "ge_cloud_data" : {"type": "boolean"}, - "ge_cloud_serial" : {"type": "string", "empty": False}, - "ge_cloud_key" : {"type": "string", "empty": False}, - "ge_cloud_direct" : {"type": "boolean"}, - "ge_cloud_automatic" : {"type": "boolean"}, - "num_inverters" : {"type": "integer", "zero": False}, - "balance_inverters_seconds" : {"type": "integer", "zero": False}, - "givtcp_rest" : {"type": "string_list"}, + "ge_cloud_data": {"type": "boolean"}, + "ge_cloud_serial": {"type": "string", "empty": False}, + "ge_cloud_key": {"type": "string", "empty": False}, + "ge_cloud_direct": {"type": "boolean"}, + "ge_cloud_automatic": {"type": "boolean"}, + "num_inverters": {"type": "integer", "zero": False}, + "balance_inverters_seconds": {"type": "integer", "zero": False}, + "givtcp_rest": {"type": "string_list"}, "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "battery_power": {"type": "sensor_list", "sensor_type": "float"}, @@ -1558,6 +1549,3 @@ "octopus_saving_session_octopoints_per_penny": {"type": "integer"}, "octopus_free_url": {"type": "string", "empty": False}, } - - - diff --git a/apps/predbat/gecloud.py b/apps/predbat/gecloud.py index 60ee85c7..98aeac96 100644 --- a/apps/predbat/gecloud.py +++ b/apps/predbat/gecloud.py @@ -564,8 +564,8 @@ async def async_automatic_config(self, devices): self.base.args["pause_mode"] = ["select.predbat_gecloud_" + device + "_pause_battery" for device in batteries] self.base.args["pause_start_time"] = ["select.predbat_gecloud_" + device + "_pause_battery_start_time" for device in batteries] self.base.args["pause_end_time"] = ["select.predbat_gecloud_" + device + "_pause_battery_end_time" for device in batteries] - if 'givtcp_rest' in self.base.args: - del self.base.args['givtcp_rest'] + if "givtcp_rest" in self.base.args: + del self.base.args["givtcp_rest"] self.base.args["ge_cloud_serial"] = batteries[0] # reconfigure for EMS diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 02ce7dbd..8008e97d 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -228,8 +228,8 @@ def get_state_wrapper(self, entity_id=None, default=None, attribute=None, refres return None # Entity with coded attribute - if entity_id and '$' in entity_id: - entity_id, attribute = entity_id.split('$') + if entity_id and "$" in entity_id: + entity_id, attribute = entity_id.split("$") return self.ha_interface.get_state(entity_id=entity_id, default=default, attribute=attribute, refresh=refresh) @@ -955,13 +955,13 @@ def validate_config(self): self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a string".format(name, sensor)) errors += 1 break - if '.' not in sensor: + if "." not in sensor: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id".format(name, sensor)) errors += 1 break if spec.get("modify", False): prefix = sensor.split(".")[0] - if prefix not in ['switch', 'select', 'input_number', 'number']: + if prefix not in ["switch", "select", "input_number", "number"]: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} can not be modified".format(name, sensor)) errors += 1 break From bb2156a4c81c7e4e3a48e64a2addc30edbb71d23 Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sun, 23 Mar 2025 13:00:19 +0000 Subject: [PATCH 3/8] Updates --- apps/predbat/config.py | 102 +++++++++++++++++++++++++++++----------- apps/predbat/predbat.py | 98 +++++++++++++++++++++++++++++++------- apps/predbat/web.py | 6 ++- 3 files changed, 160 insertions(+), 46 deletions(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index 35259407..bf6596f3 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1460,41 +1460,47 @@ # Apps.yaml validation schema APPS_SCHEMA = { "currency_symbols": {"type": "string|string_list"}, - "db_enable": {"type": "boolean"}, - "db_days": {"type": "integer"}, - "db_mirror_ha": {"type": "boolean"}, - "db_primary": {"type": "boolean"}, - "threads": {"type": "string", "allowed": ["auto", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}, - "ha_url": {"type": "string", "empty": False}, - "ha_key": {"type": "string", "empty": False}, - "load_filter_threshold": {"type": "integer"}, - "web_port": {"type": "integer"}, - "load_today": {"type": "sensor|sensor_list", "sensor_type": "float", "required": True}, - "import_today": { + "db_enable" : {"type": "boolean"}, + "db_days" : {"type": "integer"}, + "db_mirror_ha" : {"type": "boolean"}, + "db_primary" : {"type": "boolean"}, + "threads" : {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, + "ha_url" : {"type": "string", "empty": False}, + "ha_key" : {"type": "string", "empty": False}, + "load_filter_threshold" : {"type": "integer"}, + "web_port" : {"type": "integer"}, + "load_today" : { "type": "sensor|sensor_list", "sensor_type": "float", + "required": True }, - "export_today": { + "import_today" : { "type": "sensor|sensor_list", "sensor_type": "float", }, - "pv_today": { + "export_today" : { "type": "sensor|sensor_list", "sensor_type": "float", }, - "load_forecast_only": {"type": "boolean"}, - "load_forecast": { + "pv_today" : { "type": "sensor|sensor_list", "sensor_type": "float", }, - "ge_cloud_data": {"type": "boolean"}, - "ge_cloud_serial": {"type": "string", "empty": False}, - "ge_cloud_key": {"type": "string", "empty": False}, - "ge_cloud_direct": {"type": "boolean"}, - "ge_cloud_automatic": {"type": "boolean"}, - "num_inverters": {"type": "integer", "zero": False}, - "balance_inverters_seconds": {"type": "integer", "zero": False}, - "givtcp_rest": {"type": "string_list"}, + "load_forecast_only" : { + "type": "boolean" + }, + "load_forecast" : { + "type": "sensor|sensor_list", + "sensor_type": "dict", + }, + "ge_cloud_data" : {"type": "boolean"}, + "ge_cloud_serial" : {"type": "string", "empty": False}, + "ge_cloud_key" : {"type": "string", "empty": False}, + "ge_cloud_direct" : {"type": "boolean"}, + "ge_cloud_automatic" : {"type": "boolean"}, + "num_inverters" : {"type": "integer", "zero": False}, + "balance_inverters_seconds" : {"type": "integer", "zero": False}, + "givtcp_rest" : {"type": "string_list"}, "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "battery_power": {"type": "sensor_list", "sensor_type": "float"}, @@ -1505,6 +1511,7 @@ "reserve": {"type": "sensor_list", "sensor_type": "float"}, "inverter_mode": {"type": "sensor_list", "sensor_type": "string", "modify": True}, "inverter_time": {"type": "sensor_list", "sensor_type": "string"}, + "inverter_type": {"type": "string_list"}, "charge_start_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, "charge_end_time": {"type": "sensor_list", "sensor_type": "string", "modify": True}, "charge_limit": {"type": "sensor_list", "sensor_type": "float", "modify": True}, @@ -1537,15 +1544,56 @@ "car_charging_planned_response": {"type": "string_list"}, "car_charging_now": {"type": "sensor|sensor_list", "sensor_type": "string"}, "car_charging_now_response": {"type": "string_list"}, - "car_charging_battery_size": {"type": "integer|sensor", "zero": False}, - "car_charging_soc": {"type": "sensor|integer"}, - "car_charging_limit": {"type": "integer|sensor"}, + "car_charging_battery_size": {"type": "float|sensor", "zero": False, "sensor_type": "float"}, + "car_charging_soc": {"type": "float|sensor", "sensor_type": "float"}, + "car_charging_limit": {"type": "float|sensor", "sensor_type": "float"}, "car_charging_exclusive": {"type": "boolean_list"}, - "carbon_intensity": {"type": "sensor", "sensor_type": "float"}, + "carbon_intensity": {"type": "sensor", "sensor_type": "string"}, "octopus_intelligent_slot": {"type": "sensor", "sensor_type": "switch"}, "octopus_ready_time": {"type": "sensor", "sensor_type": "string"}, "octopus_charge_limit": {"type": "sensor", "sensor_type": "float"}, "octopus_slot_low_rate": {"type": "boolean"}, "octopus_saving_session_octopoints_per_penny": {"type": "integer"}, "octopus_free_url": {"type": "string", "empty": False}, + "metric_octopus_import": {"type": "sensor", "sensor_type": "float"}, + "metric_octopus_export": {"type": "sensor", "sensor_type": "float"}, + "octopus_api_key": {"type": "string", "empty": False}, + "octopus_api_account": {"type": "string", "empty": False}, + "rates_import": {"type": "dict_list"}, + "rates_export": {"type": "dict_list"}, + "alerts": {"type": "dict"}, + "rates_import_octopus_url": {"type": "string", "empty": False}, + "rates_export_octopus_url": {"type": "string", "empty": False}, + "rates_import_override": {"type": "dict_list"}, + "rates_export_override": {"type": "dict_list"}, + "days_previous": {"type": "integer_list"}, + "days_previous_weight": {"type": "integer_list"}, + "forecast_hours": {"type": "integer"}, + "notify_devices": {"type": "string_list"}, + "battery_scaling": {"type": "float_list"}, + "import_export_scaling": {"type": "float"}, + "export_triggers": {"type": "dict_list"}, + "iboost_energy_today": {"type": "sensor", "sensor_type": "float"}, + "metric_octopus_gas": {"type": "sensor", "sensor_type": "float"}, + "rates_gas": {"type": "dict_list"}, + "futurerate_url": {"type": "string", "empty": False}, + "futurerate_adjust_import": {"type": "boolean"}, + "futurerate_adjust_export": {"type": "boolean"}, + "futurerate_peak_start": {"type": "string", "empty": False}, + "futurerate_peak_end": {"type": "string", "empty": False}, + "octopus_region": {"type": "string", "empty": False}, + "compare_list": {"type": "dict_list"}, + "watch_list": {"type": "string_list"}, + "charge_start_service": {"type": "dict_list"}, + "charge_stop_service": {"type": "dict_list"}, + "discharge_start_service": {"type": "dict_list"}, + "discharge_stop_service": {"type": "dict_list"}, + "charge_freeze_service": {"type": "dict_list"}, + "discharge_freeze_service": {"type": "dict_list"}, + "device_id": {"type": "string", "empty": False}, + "predheat": {"type": "dict"}, + } + + + diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 8008e97d..a7fa6a7b 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -228,8 +228,8 @@ def get_state_wrapper(self, entity_id=None, default=None, attribute=None, refres return None # Entity with coded attribute - if entity_id and "$" in entity_id: - entity_id, attribute = entity_id.split("$") + if entity_id and '$' in entity_id: + entity_id, attribute = entity_id.split('$') return self.ha_interface.get_state(entity_id=entity_id, default=default, attribute=attribute, refresh=refresh) @@ -294,6 +294,7 @@ def reset(self): """ reset_prediction_globals() self.api_errors = 0 + self.arg_errors = {} self.ha_interface = None self.fatal_error = False self.ge_cloud_direct = None @@ -875,6 +876,7 @@ def validate_config(self): Uses APPS_SCHEMA to validate the self.args configuration read from apps.yaml """ errors = 0 + self.arg_errors = {} for name in APPS_SCHEMA: spec = APPS_SCHEMA[name] required = spec.get("required", False) @@ -899,18 +901,30 @@ def validate_config(self): for item in value: if not isinstance(item, int): self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not an integer".format(name, item)) + self.arg_errors[name] = "Invalid type, expected integer item {}".format(item) errors += 1 break if spec.get("zero", False) and value == 0: self.log("Warn: Validation of apps.yaml found configuration item '{}' is zero".format(name)) + self.arg_errors[name] = "Invalid value, expected non-zero integer item {}".format(item) + errors += 1 + break + elif expected_type == "float" or expected_type == "float_list": + if expected_type == "float" and (isinstance(value, float) or isinstance(value, int)): + value = [value] + if isinstance(value, list): + matches = True + for item in value: + if not isinstance(item, float) and not isinstance(item, int): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a float".format(name, item)) + self.arg_errors[name] = "Invalid type, expected float item {}".format(item) errors += 1 break - elif expected_type == "float": - matches = isinstance(value, float) elif expected_type == "string": matches = isinstance(value, str) if matches and spec.get("empty", False) and not value: self.log("Warn: Validation of apps.yaml found configuration item '{}' is empty".format(name)) + self.arg_errors[name] = "Invalid value, expected non-empty string" errors += 1 break elif expected_type == "boolean" or expected_type == "boolean_list": @@ -921,20 +935,43 @@ def validate_config(self): for item in value: if not isinstance(item, bool): self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a boolean".format(name, item)) + self.arg_errors[name] = "Invalid type, expected boolean item {}".format(item) + errors += 1 + break + elif expected_type == "integer" or expected_type == "integer_list": + if expected_type == "integer" and isinstance(value, int): + value = [value] + if isinstance(value, list): + matches = True + for item in value: + if not isinstance(item, bool): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not an integer".format(name, item)) + self.arg_errors[name] = "Invalid type, expected integer item {}".format(item) + errors += 1 + break + elif expected_type == "dict" or expected_type == "dict_list": + if expected_type == "dict" and isinstance(value, dict): + value = [value] + if isinstance(value, list): + matches = True + for item in value: + if not isinstance(item, dict): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not an dict".format(name, item)) + self.arg_errors[name] = "Invalid type, element {} expected dict".format(item) errors += 1 break - elif expected_type == "list": - matches = isinstance(value, list) elif expected_type == "int_float_dict": if instance(value, dict): matches = True for key in value: if not isinstance(key, int): self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} value {} is not a int => float".format(name, key, value)) + self.arg_errors[name] = "Invalid element key {} expected int".format(key) errors += 1 break - if not isinstance(value[key], float): + if not isinstance(value[key], float) and not isinstance(value[key], int): self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} value {} is not a int => float".format(name, key, value)) + self.arg_errors[name] = "Invalid element key {} value {}, expected int => float".format(key, value[key]) errors += 1 break elif expected_type == "string_list": @@ -943,6 +980,7 @@ def validate_config(self): for item in value: if not isinstance(item, str): self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a string".format(name, item)) + self.arg_errors[name] = "Invalid type, expected string" errors += 1 break elif expected_type == "sensor_list" or expected_type == "sensor": @@ -951,44 +989,67 @@ def validate_config(self): if isinstance(value, list): matches = True for sensor in value: + sensor_type = spec.get("sensor_type", None) + if sensor_type in ["integer", "float"] and isinstance(sensor, int) and not spec.get("modify", False): + # Allow fixed integer values + continue + if sensor_type == "float" and isinstance(sensor, float) and not spec.get("modify", False): + # Allow fixed float values + continue + if sensor_type == "string" and isinstance(sensor, str) and not spec.get("modify", False) and not '.' in sensor: + # Allow fixed string values + continue + if not isinstance(sensor, str): - self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a string".format(name, sensor)) + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id (must be a string)".format(name, sensor)) + self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break - if "." not in sensor: - self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id".format(name, sensor)) + if '.' not in sensor: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id (must contain a dot)".format(name, sensor)) + self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break if spec.get("modify", False): prefix = sensor.split(".")[0] - if prefix not in ["switch", "select", "input_number", "number"]: + if prefix not in ['switch', 'select', 'input_number', 'number']: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} can not be modified".format(name, sensor)) + self.arg_errors[name] = "Invalid entity_id in element {}, can not be modified".format(sensor) errors += 1 break state = self.get_state_wrapper(sensor) - sensor_type = spec.get("sensor_type", None) if state is None: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} returned value None".format(name, sensor)) + self.arg_errors[name] = "Invalid value None in element {}".format(sensor) errors += 1 break if sensor_type and sensor_type == "float": try: float(state) - except ValueError: - self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a float".format(name, sensor)) + except (ValueError, TypeError) as e: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a float got {}".format(name, sensor, state)) + self.arg_errors[name] = "Invalid value in element {}, expected float".format(sensor) errors += 1 break elif sensor_type and sensor_type == "integer": try: int(state) - except ValueError: - self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not an integer".format(name, sensor)) + except (ValueError, TypeError) as e: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not an integer got {}".format(name, sensor, state)) + self.arg_errors[name] = "Invalid value in element {}, expected integer".format(sensor) errors += 1 break elif sensor_type and sensor_type == "switch": if state not in ["on", "off", True, False]: - self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a switch".format(name, sensor)) + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a switch got {}".format(name, sensor, state)) + self.arg_errors[name] = "Invalid value in element {}, expected switch".format(sensor) + errors += 1 + break + elif sensor_type and sensor_type == "dict": + if not isinstance(state, dict): + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a dict got {}".format(name, sensor, state)) + self.arg_errors[name] = "Invalid type in element {}, expected dict".format(sensor) errors += 1 break @@ -996,14 +1057,15 @@ def validate_config(self): break if not matches: self.log("Warn: Validation of apps.yaml found configuration item '{}' is not of type '{}' value was {}".format(name, expected_type, value)) + self.arg_errors[name] = "Invalid type, expected {}".format(expected_type) errors += 1 elif allowed: if value not in allowed: self.log("Warn: Validation of apps.yaml found configuration item '{}' value {} is not in allowed list {}".format(name, value, allowed)) + self.arg_errors[name] = "Invalid value, expected one of {}".format(allowed) errors += 1 if errors: self.log("Error: Validation of apps.yaml found {} configuration errors".format(errors)) - self.record_status("Error: Validation of apps.yaml found {} configuration errors".format(errors)) else: self.log("Validation of apps.yaml completed with no errors") diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 700ccabb..74bff4db 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -923,7 +923,11 @@ async def html_apps(self, request): value = args[arg] if "api_key" in arg or "cloud_key" in arg: value = ' (hidden)'.format(value) - text += "{}{}\n".format(arg, self.render_type(arg, value)) + arg_errors = self.base.arg_errors.get(arg, "") + if arg_errors: + text += "⚠{}{}\n".format(arg_errors, arg, self.render_type(arg, value)) + else: + text += "{}{}\n".format(arg, self.render_type(arg, value)) args = self.base.unmatched_args for arg in args: value = args[arg] From d18d2ce05f9d3a8d108af489a5de034d1a05ba83 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 13:01:02 +0000 Subject: [PATCH 4/8] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/config.py | 56 +++++++++++++++++------------------------ apps/predbat/predbat.py | 10 ++++---- apps/predbat/web.py | 2 +- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index bf6596f3..9f18f144 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1460,47 +1460,41 @@ # Apps.yaml validation schema APPS_SCHEMA = { "currency_symbols": {"type": "string|string_list"}, - "db_enable" : {"type": "boolean"}, - "db_days" : {"type": "integer"}, - "db_mirror_ha" : {"type": "boolean"}, - "db_primary" : {"type": "boolean"}, - "threads" : {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, - "ha_url" : {"type": "string", "empty": False}, - "ha_key" : {"type": "string", "empty": False}, - "load_filter_threshold" : {"type": "integer"}, - "web_port" : {"type": "integer"}, - "load_today" : { + "db_enable": {"type": "boolean"}, + "db_days": {"type": "integer"}, + "db_mirror_ha": {"type": "boolean"}, + "db_primary": {"type": "boolean"}, + "threads": {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, + "ha_url": {"type": "string", "empty": False}, + "ha_key": {"type": "string", "empty": False}, + "load_filter_threshold": {"type": "integer"}, + "web_port": {"type": "integer"}, + "load_today": {"type": "sensor|sensor_list", "sensor_type": "float", "required": True}, + "import_today": { "type": "sensor|sensor_list", "sensor_type": "float", - "required": True }, - "import_today" : { + "export_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "export_today" : { + "pv_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "pv_today" : { - "type": "sensor|sensor_list", - "sensor_type": "float", - }, - "load_forecast_only" : { - "type": "boolean" - }, - "load_forecast" : { + "load_forecast_only": {"type": "boolean"}, + "load_forecast": { "type": "sensor|sensor_list", "sensor_type": "dict", }, - "ge_cloud_data" : {"type": "boolean"}, - "ge_cloud_serial" : {"type": "string", "empty": False}, - "ge_cloud_key" : {"type": "string", "empty": False}, - "ge_cloud_direct" : {"type": "boolean"}, - "ge_cloud_automatic" : {"type": "boolean"}, - "num_inverters" : {"type": "integer", "zero": False}, - "balance_inverters_seconds" : {"type": "integer", "zero": False}, - "givtcp_rest" : {"type": "string_list"}, + "ge_cloud_data": {"type": "boolean"}, + "ge_cloud_serial": {"type": "string", "empty": False}, + "ge_cloud_key": {"type": "string", "empty": False}, + "ge_cloud_direct": {"type": "boolean"}, + "ge_cloud_automatic": {"type": "boolean"}, + "num_inverters": {"type": "integer", "zero": False}, + "balance_inverters_seconds": {"type": "integer", "zero": False}, + "givtcp_rest": {"type": "string_list"}, "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "battery_power": {"type": "sensor_list", "sensor_type": "float"}, @@ -1592,8 +1586,4 @@ "discharge_freeze_service": {"type": "dict_list"}, "device_id": {"type": "string", "empty": False}, "predheat": {"type": "dict"}, - } - - - diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index a7fa6a7b..95fc4e7c 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -228,8 +228,8 @@ def get_state_wrapper(self, entity_id=None, default=None, attribute=None, refres return None # Entity with coded attribute - if entity_id and '$' in entity_id: - entity_id, attribute = entity_id.split('$') + if entity_id and "$" in entity_id: + entity_id, attribute = entity_id.split("$") return self.ha_interface.get_state(entity_id=entity_id, default=default, attribute=attribute, refresh=refresh) @@ -996,7 +996,7 @@ def validate_config(self): if sensor_type == "float" and isinstance(sensor, float) and not spec.get("modify", False): # Allow fixed float values continue - if sensor_type == "string" and isinstance(sensor, str) and not spec.get("modify", False) and not '.' in sensor: + if sensor_type == "string" and isinstance(sensor, str) and not spec.get("modify", False) and not "." in sensor: # Allow fixed string values continue @@ -1005,14 +1005,14 @@ def validate_config(self): self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break - if '.' not in sensor: + if "." not in sensor: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id (must contain a dot)".format(name, sensor)) self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break if spec.get("modify", False): prefix = sensor.split(".")[0] - if prefix not in ['switch', 'select', 'input_number', 'number']: + if prefix not in ["switch", "select", "input_number", "number"]: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} can not be modified".format(name, sensor)) self.arg_errors[name] = "Invalid entity_id in element {}, can not be modified".format(sensor) errors += 1 diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 74bff4db..9fe05588 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -925,7 +925,7 @@ async def html_apps(self, request): value = ' (hidden)'.format(value) arg_errors = self.base.arg_errors.get(arg, "") if arg_errors: - text += "⚠{}{}\n".format(arg_errors, arg, self.render_type(arg, value)) + text += '⚠{}{}\n'.format(arg_errors, arg, self.render_type(arg, value)) else: text += "{}{}\n".format(arg, self.render_type(arg, value)) args = self.base.unmatched_args From 6718294138d331e46b18e350803f4f80e44c416a Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:43:27 +0000 Subject: [PATCH 5/8] Web UI improvements to apps.yaml check --- apps/predbat/config.py | 58 ++++++++++++++++++++++++----------------- apps/predbat/output.py | 2 +- apps/predbat/predbat.py | 22 +++++++++------- apps/predbat/web.py | 22 +++++++++++++--- 4 files changed, 66 insertions(+), 38 deletions(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index 9f18f144..b70dc9c0 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1460,41 +1460,47 @@ # Apps.yaml validation schema APPS_SCHEMA = { "currency_symbols": {"type": "string|string_list"}, - "db_enable": {"type": "boolean"}, - "db_days": {"type": "integer"}, - "db_mirror_ha": {"type": "boolean"}, - "db_primary": {"type": "boolean"}, - "threads": {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, - "ha_url": {"type": "string", "empty": False}, - "ha_key": {"type": "string", "empty": False}, - "load_filter_threshold": {"type": "integer"}, - "web_port": {"type": "integer"}, - "load_today": {"type": "sensor|sensor_list", "sensor_type": "float", "required": True}, - "import_today": { + "db_enable" : {"type": "boolean"}, + "db_days" : {"type": "integer"}, + "db_mirror_ha" : {"type": "boolean"}, + "db_primary" : {"type": "boolean"}, + "threads" : {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, + "ha_url" : {"type": "string", "empty": False}, + "ha_key" : {"type": "string", "empty": False}, + "load_filter_threshold" : {"type": "integer"}, + "web_port" : {"type": "integer"}, + "load_today" : { "type": "sensor|sensor_list", "sensor_type": "float", + "required": True }, - "export_today": { + "import_today" : { "type": "sensor|sensor_list", "sensor_type": "float", }, - "pv_today": { + "export_today" : { "type": "sensor|sensor_list", "sensor_type": "float", }, - "load_forecast_only": {"type": "boolean"}, - "load_forecast": { + "pv_today" : { + "type": "sensor|sensor_list", + "sensor_type": "float", + }, + "load_forecast_only" : { + "type": "boolean" + }, + "load_forecast" : { "type": "sensor|sensor_list", "sensor_type": "dict", }, - "ge_cloud_data": {"type": "boolean"}, - "ge_cloud_serial": {"type": "string", "empty": False}, - "ge_cloud_key": {"type": "string", "empty": False}, - "ge_cloud_direct": {"type": "boolean"}, - "ge_cloud_automatic": {"type": "boolean"}, - "num_inverters": {"type": "integer", "zero": False}, - "balance_inverters_seconds": {"type": "integer", "zero": False}, - "givtcp_rest": {"type": "string_list"}, + "ge_cloud_data" : {"type": "boolean"}, + "ge_cloud_serial" : {"type": "string", "empty": False}, + "ge_cloud_key" : {"type": "string", "empty": False}, + "ge_cloud_direct" : {"type": "boolean"}, + "ge_cloud_automatic" : {"type": "boolean"}, + "num_inverters" : {"type": "integer", "zero": False}, + "balance_inverters_seconds" : {"type": "integer", "zero": False}, + "givtcp_rest" : {"type": "string_list"}, "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "battery_power": {"type": "sensor_list", "sensor_type": "float"}, @@ -1577,7 +1583,7 @@ "futurerate_peak_end": {"type": "string", "empty": False}, "octopus_region": {"type": "string", "empty": False}, "compare_list": {"type": "dict_list"}, - "watch_list": {"type": "string_list"}, + "watch_list": {"type": "sensor_list"}, "charge_start_service": {"type": "dict_list"}, "charge_stop_service": {"type": "dict_list"}, "discharge_start_service": {"type": "dict_list"}, @@ -1586,4 +1592,8 @@ "discharge_freeze_service": {"type": "dict_list"}, "device_id": {"type": "string", "empty": False}, "predheat": {"type": "dict"}, + } + + + diff --git a/apps/predbat/output.py b/apps/predbat/output.py index bcc4c204..75cd6b7b 100644 --- a/apps/predbat/output.py +++ b/apps/predbat/output.py @@ -1754,6 +1754,7 @@ def record_status(self, message, debug="", had_errors=False, notify=False, extra self.current_status = message + extra if notify and self.previous_status != message and self.set_status_notify: self.call_notify("Predbat status change to: " + message + extra) + self.previous_status = message self.dashboard_item( self.prefix + ".status", @@ -1774,7 +1775,6 @@ def record_status(self, message, debug="", had_errors=False, notify=False, extra else: self.log("Info: record_status {}".format(message + extra)) - self.previous_status = message if had_errors: self.had_errors = True diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 95fc4e7c..112f0559 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -228,8 +228,8 @@ def get_state_wrapper(self, entity_id=None, default=None, attribute=None, refres return None # Entity with coded attribute - if entity_id and "$" in entity_id: - entity_id, attribute = entity_id.split("$") + if entity_id and '$' in entity_id: + entity_id, attribute = entity_id.split('$') return self.ha_interface.get_state(entity_id=entity_id, default=default, attribute=attribute, refresh=refresh) @@ -996,7 +996,7 @@ def validate_config(self): if sensor_type == "float" and isinstance(sensor, float) and not spec.get("modify", False): # Allow fixed float values continue - if sensor_type == "string" and isinstance(sensor, str) and not spec.get("modify", False) and not "." in sensor: + if sensor_type == "string" and isinstance(sensor, str) and not spec.get("modify", False) and not '.' in sensor: # Allow fixed string values continue @@ -1005,18 +1005,22 @@ def validate_config(self): self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break - if "." not in sensor: + if '.' not in sensor: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id (must contain a dot)".format(name, sensor)) self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break if spec.get("modify", False): prefix = sensor.split(".")[0] - if prefix not in ["switch", "select", "input_number", "number"]: - self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} can not be modified".format(name, sensor)) - self.arg_errors[name] = "Invalid entity_id in element {}, can not be modified".format(sensor) - errors += 1 - break + if prefix not in ['switch', 'select', 'input_number', 'number']: + if sensor.startswith("sensor.predbat_"): + # We can ignore predbat generated sensors as they are control placeholders + pass + else: + self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} can not be modified".format(name, sensor)) + self.arg_errors[name] = "Invalid entity_id in element {}, can not be modified".format(sensor) + errors += 1 + break state = self.get_state_wrapper(sensor) if state is None: diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 9fe05588..2be05ec7 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -199,12 +199,20 @@ def get_status_html(self, level, status, debug_enable, read_only, mode, version) text += "

Status

\n" text += "\n" - text += "\n".format(status) + if ('Warn:' in status) or ('Error:' in status): + text += "\n".format(status) + else: + text += "\n".format(status) text += "\n".format(version) text += "\n".format(mode) text += "\n".format(level) text += "\n".format(debug_enable) text += "\n".format(read_only) + if self.base.arg_errors: + count_errors = len(self.base.arg_errors) + text += "\n".format(count_errors) + else: + text += "\n" text += "
Status{}
Status{}
Status{}
Version{}
Mode{}
SOC{}%
Debug Enable{}
Set Read Only{}
Configapps.yaml has {} errors
ConfigOK
\n" text += "\n" text += "

Debug

\n" @@ -914,7 +922,10 @@ async def html_apps(self, request): self.default_page = "./apps" text = self.get_header("Predbat Apps.yaml", refresh=60 * 5) text += "\n" - text += "apps.yaml
\n" + warning = "" + if self.base.arg_errors: + warning = "⚠" + text += "{}apps.yaml - has {} errors
\n".format(warning, len(self.base.arg_errors)) text += "
\n" text += "\n'.format(arg_errors, arg, self.render_type(arg, value)) + text += "\n".format(arg_errors, arg, self.render_type(arg, value)) else: text += "\n".format(arg, self.render_type(arg, value)) args = self.base.unmatched_args @@ -1168,7 +1179,10 @@ async def html_menu(self, request): text += '\n' text += '\n' text += '\n' - text += '\n' + warning = "" + if self.base.arg_errors: + warning = "⚠ " + text += '\n'.format(warning) text += '\n' text += '\n' text += '\n' From a0a3e4e405066ea29d29b33f2835a537fa7006d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:44:09 +0000 Subject: [PATCH 6/8] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/config.py | 56 +++++++++++++++++------------------------ apps/predbat/predbat.py | 10 ++++---- apps/predbat/web.py | 4 +-- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index b70dc9c0..fbc753a9 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1460,47 +1460,41 @@ # Apps.yaml validation schema APPS_SCHEMA = { "currency_symbols": {"type": "string|string_list"}, - "db_enable" : {"type": "boolean"}, - "db_days" : {"type": "integer"}, - "db_mirror_ha" : {"type": "boolean"}, - "db_primary" : {"type": "boolean"}, - "threads" : {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, - "ha_url" : {"type": "string", "empty": False}, - "ha_key" : {"type": "string", "empty": False}, - "load_filter_threshold" : {"type": "integer"}, - "web_port" : {"type": "integer"}, - "load_today" : { + "db_enable": {"type": "boolean"}, + "db_days": {"type": "integer"}, + "db_mirror_ha": {"type": "boolean"}, + "db_primary": {"type": "boolean"}, + "threads": {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, + "ha_url": {"type": "string", "empty": False}, + "ha_key": {"type": "string", "empty": False}, + "load_filter_threshold": {"type": "integer"}, + "web_port": {"type": "integer"}, + "load_today": {"type": "sensor|sensor_list", "sensor_type": "float", "required": True}, + "import_today": { "type": "sensor|sensor_list", "sensor_type": "float", - "required": True }, - "import_today" : { + "export_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "export_today" : { + "pv_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "pv_today" : { - "type": "sensor|sensor_list", - "sensor_type": "float", - }, - "load_forecast_only" : { - "type": "boolean" - }, - "load_forecast" : { + "load_forecast_only": {"type": "boolean"}, + "load_forecast": { "type": "sensor|sensor_list", "sensor_type": "dict", }, - "ge_cloud_data" : {"type": "boolean"}, - "ge_cloud_serial" : {"type": "string", "empty": False}, - "ge_cloud_key" : {"type": "string", "empty": False}, - "ge_cloud_direct" : {"type": "boolean"}, - "ge_cloud_automatic" : {"type": "boolean"}, - "num_inverters" : {"type": "integer", "zero": False}, - "balance_inverters_seconds" : {"type": "integer", "zero": False}, - "givtcp_rest" : {"type": "string_list"}, + "ge_cloud_data": {"type": "boolean"}, + "ge_cloud_serial": {"type": "string", "empty": False}, + "ge_cloud_key": {"type": "string", "empty": False}, + "ge_cloud_direct": {"type": "boolean"}, + "ge_cloud_automatic": {"type": "boolean"}, + "num_inverters": {"type": "integer", "zero": False}, + "balance_inverters_seconds": {"type": "integer", "zero": False}, + "givtcp_rest": {"type": "string_list"}, "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "battery_power": {"type": "sensor_list", "sensor_type": "float"}, @@ -1592,8 +1586,4 @@ "discharge_freeze_service": {"type": "dict_list"}, "device_id": {"type": "string", "empty": False}, "predheat": {"type": "dict"}, - } - - - diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 112f0559..747731ee 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -228,8 +228,8 @@ def get_state_wrapper(self, entity_id=None, default=None, attribute=None, refres return None # Entity with coded attribute - if entity_id and '$' in entity_id: - entity_id, attribute = entity_id.split('$') + if entity_id and "$" in entity_id: + entity_id, attribute = entity_id.split("$") return self.ha_interface.get_state(entity_id=entity_id, default=default, attribute=attribute, refresh=refresh) @@ -996,7 +996,7 @@ def validate_config(self): if sensor_type == "float" and isinstance(sensor, float) and not spec.get("modify", False): # Allow fixed float values continue - if sensor_type == "string" and isinstance(sensor, str) and not spec.get("modify", False) and not '.' in sensor: + if sensor_type == "string" and isinstance(sensor, str) and not spec.get("modify", False) and not "." in sensor: # Allow fixed string values continue @@ -1005,14 +1005,14 @@ def validate_config(self): self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break - if '.' not in sensor: + if "." not in sensor: self.log("Warn: Validation of apps.yaml found configuration item '{}' element {} is not a valid entity_id (must contain a dot)".format(name, sensor)) self.arg_errors[name] = "Invalid entity_id in element {}".format(sensor) errors += 1 break if spec.get("modify", False): prefix = sensor.split(".")[0] - if prefix not in ['switch', 'select', 'input_number', 'number']: + if prefix not in ["switch", "select", "input_number", "number"]: if sensor.startswith("sensor.predbat_"): # We can ignore predbat generated sensors as they are control placeholders pass diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 2be05ec7..ab4e5430 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -199,7 +199,7 @@ def get_status_html(self, level, status, debug_enable, read_only, mode, version) text += "

Status

\n" text += "
NameValue\n" @@ -925,7 +936,7 @@ async def html_apps(self, request): value = ' (hidden)'.format(value) arg_errors = self.base.arg_errors.get(arg, "") if arg_errors: - text += '
⚠{}{}
⚠{}{}
{}{}
PlanChartsConfigapps.yaml{}apps.yamlLogCompareDocs
\n" - if ('Warn:' in status) or ('Error:' in status): + if ("Warn:" in status) or ("Error:" in status): text += "\n".format(status) else: text += "\n".format(status) @@ -936,7 +936,7 @@ async def html_apps(self, request): value = ' (hidden)'.format(value) arg_errors = self.base.arg_errors.get(arg, "") if arg_errors: - text += "\n".format(arg_errors, arg, self.render_type(arg, value)) + text += '\n'.format(arg_errors, arg, self.render_type(arg, value)) else: text += "\n".format(arg, self.render_type(arg, value)) args = self.base.unmatched_args From e4127b8be6f3813bb1b06fe3fc560273f0cc81ee Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:45:29 +0000 Subject: [PATCH 7/8] Updated --- apps/predbat/config.py | 58 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index fbc753a9..bf6596f3 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1460,41 +1460,47 @@ # Apps.yaml validation schema APPS_SCHEMA = { "currency_symbols": {"type": "string|string_list"}, - "db_enable": {"type": "boolean"}, - "db_days": {"type": "integer"}, - "db_mirror_ha": {"type": "boolean"}, - "db_primary": {"type": "boolean"}, - "threads": {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, - "ha_url": {"type": "string", "empty": False}, - "ha_key": {"type": "string", "empty": False}, - "load_filter_threshold": {"type": "integer"}, - "web_port": {"type": "integer"}, - "load_today": {"type": "sensor|sensor_list", "sensor_type": "float", "required": True}, - "import_today": { + "db_enable" : {"type": "boolean"}, + "db_days" : {"type": "integer"}, + "db_mirror_ha" : {"type": "boolean"}, + "db_primary" : {"type": "boolean"}, + "threads" : {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, + "ha_url" : {"type": "string", "empty": False}, + "ha_key" : {"type": "string", "empty": False}, + "load_filter_threshold" : {"type": "integer"}, + "web_port" : {"type": "integer"}, + "load_today" : { "type": "sensor|sensor_list", "sensor_type": "float", + "required": True }, - "export_today": { + "import_today" : { "type": "sensor|sensor_list", "sensor_type": "float", }, - "pv_today": { + "export_today" : { "type": "sensor|sensor_list", "sensor_type": "float", }, - "load_forecast_only": {"type": "boolean"}, - "load_forecast": { + "pv_today" : { + "type": "sensor|sensor_list", + "sensor_type": "float", + }, + "load_forecast_only" : { + "type": "boolean" + }, + "load_forecast" : { "type": "sensor|sensor_list", "sensor_type": "dict", }, - "ge_cloud_data": {"type": "boolean"}, - "ge_cloud_serial": {"type": "string", "empty": False}, - "ge_cloud_key": {"type": "string", "empty": False}, - "ge_cloud_direct": {"type": "boolean"}, - "ge_cloud_automatic": {"type": "boolean"}, - "num_inverters": {"type": "integer", "zero": False}, - "balance_inverters_seconds": {"type": "integer", "zero": False}, - "givtcp_rest": {"type": "string_list"}, + "ge_cloud_data" : {"type": "boolean"}, + "ge_cloud_serial" : {"type": "string", "empty": False}, + "ge_cloud_key" : {"type": "string", "empty": False}, + "ge_cloud_direct" : {"type": "boolean"}, + "ge_cloud_automatic" : {"type": "boolean"}, + "num_inverters" : {"type": "integer", "zero": False}, + "balance_inverters_seconds" : {"type": "integer", "zero": False}, + "givtcp_rest" : {"type": "string_list"}, "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "battery_power": {"type": "sensor_list", "sensor_type": "float"}, @@ -1577,7 +1583,7 @@ "futurerate_peak_end": {"type": "string", "empty": False}, "octopus_region": {"type": "string", "empty": False}, "compare_list": {"type": "dict_list"}, - "watch_list": {"type": "sensor_list"}, + "watch_list": {"type": "string_list"}, "charge_start_service": {"type": "dict_list"}, "charge_stop_service": {"type": "dict_list"}, "discharge_start_service": {"type": "dict_list"}, @@ -1586,4 +1592,8 @@ "discharge_freeze_service": {"type": "dict_list"}, "device_id": {"type": "string", "empty": False}, "predheat": {"type": "dict"}, + } + + + From de40d6db33b9d8a97da00617f3554015c5975600 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:46:07 +0000 Subject: [PATCH 8/8] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/config.py | 56 +++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/apps/predbat/config.py b/apps/predbat/config.py index bf6596f3..9f18f144 100644 --- a/apps/predbat/config.py +++ b/apps/predbat/config.py @@ -1460,47 +1460,41 @@ # Apps.yaml validation schema APPS_SCHEMA = { "currency_symbols": {"type": "string|string_list"}, - "db_enable" : {"type": "boolean"}, - "db_days" : {"type": "integer"}, - "db_mirror_ha" : {"type": "boolean"}, - "db_primary" : {"type": "boolean"}, - "threads" : {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, - "ha_url" : {"type": "string", "empty": False}, - "ha_key" : {"type": "string", "empty": False}, - "load_filter_threshold" : {"type": "integer"}, - "web_port" : {"type": "integer"}, - "load_today" : { + "db_enable": {"type": "boolean"}, + "db_days": {"type": "integer"}, + "db_mirror_ha": {"type": "boolean"}, + "db_primary": {"type": "boolean"}, + "threads": {"type": "string|integer", "allowed": ["auto", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, + "ha_url": {"type": "string", "empty": False}, + "ha_key": {"type": "string", "empty": False}, + "load_filter_threshold": {"type": "integer"}, + "web_port": {"type": "integer"}, + "load_today": {"type": "sensor|sensor_list", "sensor_type": "float", "required": True}, + "import_today": { "type": "sensor|sensor_list", "sensor_type": "float", - "required": True }, - "import_today" : { + "export_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "export_today" : { + "pv_today": { "type": "sensor|sensor_list", "sensor_type": "float", }, - "pv_today" : { - "type": "sensor|sensor_list", - "sensor_type": "float", - }, - "load_forecast_only" : { - "type": "boolean" - }, - "load_forecast" : { + "load_forecast_only": {"type": "boolean"}, + "load_forecast": { "type": "sensor|sensor_list", "sensor_type": "dict", }, - "ge_cloud_data" : {"type": "boolean"}, - "ge_cloud_serial" : {"type": "string", "empty": False}, - "ge_cloud_key" : {"type": "string", "empty": False}, - "ge_cloud_direct" : {"type": "boolean"}, - "ge_cloud_automatic" : {"type": "boolean"}, - "num_inverters" : {"type": "integer", "zero": False}, - "balance_inverters_seconds" : {"type": "integer", "zero": False}, - "givtcp_rest" : {"type": "string_list"}, + "ge_cloud_data": {"type": "boolean"}, + "ge_cloud_serial": {"type": "string", "empty": False}, + "ge_cloud_key": {"type": "string", "empty": False}, + "ge_cloud_direct": {"type": "boolean"}, + "ge_cloud_automatic": {"type": "boolean"}, + "num_inverters": {"type": "integer", "zero": False}, + "balance_inverters_seconds": {"type": "integer", "zero": False}, + "givtcp_rest": {"type": "string_list"}, "charge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "discharge_rate": {"type": "sensor_list", "sensor_type": "float", "modify": True}, "battery_power": {"type": "sensor_list", "sensor_type": "float"}, @@ -1592,8 +1586,4 @@ "discharge_freeze_service": {"type": "dict_list"}, "device_id": {"type": "string", "empty": False}, "predheat": {"type": "dict"}, - } - - -
Status{}
Status{}
⚠{}{}
⚠{}{}
{}{}