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 += "Status | {} |
\n".format(status)
+ if ('Warn:' in status) or ('Error:' in status):
+ text += "Status | {} |
\n".format(status)
+ else:
+ text += "Status | {} |
\n".format(status)
text += "Version | {} |
\n".format(version)
text += "Mode | {} |
\n".format(mode)
text += "SOC | {}% |
\n".format(level)
text += "Debug Enable | {} |
\n".format(debug_enable)
text += "Set Read Only | {} |
\n".format(read_only)
+ if self.base.arg_errors:
+ count_errors = len(self.base.arg_errors)
+ text += "Config | apps.yaml has {} errors |
\n".format(count_errors)
+ else:
+ text += "Config | OK |
\n"
text += "
\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 += "Name | Value | \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 += ' |
---|
⚠{} | {} |
\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 += 'Plan | \n'
text += 'Charts | \n'
text += 'Config | \n'
- text += 'apps.yaml | \n'
+ warning = ""
+ if self.base.arg_errors:
+ warning = "⚠ "
+ text += '{}apps.yaml | \n'.format(warning)
text += 'Log | \n'
text += 'Compare | \n'
text += 'Docs | \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 += "\n"
- if ('Warn:' in status) or ('Error:' in status):
+ if ("Warn:" in status) or ("Error:" in status):
text += "Status | {} |
\n".format(status)
else:
text += "Status | {} |
\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"},
-
}
-
-
-