You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(schema): replace TBD sign conventions with source-cited values
Sign conventions for P_PV, P_PV_curtailment, P_hybrid_inverter, P_batt,
P_grid, heating_demand_heater{k}, and predicted_temp_heater{k} derived from
src/emhass/optimization.py with inline line citations. Removes the 7
maintainer-question TBD markers; adds a Sign Conventions summary table.
|`P_PV`|`_publish_standard_forecasts`| W |(TBD — see PR comment 1) | when `"P_PV"` is in DataFrame | 1:1 |`power`|`custom_pv_forecast_id`|
51
-
|`P_PV_curtailment`|`_publish_standard_forecasts`| W |(TBD — see PR comment 2) |`plant_conf.compute_curtailment = true`| 1:1 |`power`|`custom_pv_curtailment_id`|
52
-
|`P_hybrid_inverter`|`_publish_standard_forecasts`| W |(TBD — see PR comment 3) |`plant_conf.inverter_is_hybrid = true`| 1:1 |`power`|`custom_hybrid_inverter_id`|
50
+
|`P_PV`|`_publish_standard_forecasts`| W |positive = production (PV → DC bus) | when `"P_PV"` is in DataFrame | 1:1 |`power`|`custom_pv_forecast_id`. Assigned from `p_pv` forecast input at `optimization.py:2291`.|
51
+
|`P_PV_curtailment`|`_publish_standard_forecasts`| W |positive = curtailed delta (subtracts from gross PV in DC balance) |`plant_conf.compute_curtailment = true`| 1:1 |`power`|`custom_pv_curtailment_id`. CVXPY `nonneg=True` at `optimization.py:876`; appears as `(p_pv - p_pv_curtailment)` in DC-balance constraint at line 1131; bounded by `<= param_pv_forecast` at line 2761.|
52
+
|`P_hybrid_inverter`|`_publish_standard_forecasts`| W |AC-side power; positive = DC → AC delivery, negative = AC → DC (charging-from-grid via DC bus) |`plant_conf.inverter_is_hybrid = true`| 1:1 |`power`|`custom_hybrid_inverter_id`. Defined at `optimization.py:1140` as `p_hybrid_inverter == (p_dc_ac * eff_dc_ac) - (p_ac_dc * (1.0 / eff_ac_dc))`.|
53
53
|`P_deferrable{k}`|`_publish_deferrable_loads`| W | positive = consumption |`k ∈ [0, number_of_deferrable_loads)`; row skipped (error log) if column missing | 1:1 |`deferrable`|`custom_deferrable_forecast_id[k]`|
54
-
|`predicted_temp_heater{k}`|`_publish_thermal_loads`| °C | n/a (state, not flow) — (TBD which temperature: room / supply / tank — see PR comment 7) | per `k` where `def_load_config[k]` has `thermal_config` or `thermal_battery`| 1:1 |`temperature`|`custom_predicted_temperature_id[k]`|
55
-
|`heating_demand_heater{k}`|`_publish_thermal_loads`|**kWh**|(TBD — thermal vs electrical energy, see PR comment 6) | same as `predicted_temp_heater{k}`| 1:1 |`energy`|unit `"kWh"` confirmed at `utils.py` (`heating_demand_friendly_name`).`custom_heating_demand_id[k]`|
56
-
|`P_batt`|`_publish_battery_data`| W |(TBD — charge vs discharge sign, see PR comment 4) |`optim_conf.set_use_battery = true`; row skipped if column missing | 1:1 |`batt`|`custom_batt_forecast_id`|
54
+
|`predicted_temp_heater{k}`|`_publish_thermal_loads`| °C | n/a (state) | per `k` where `def_load_config[k]` has `thermal_config` or `thermal_battery`| 1:1 |`temperature`|`custom_predicted_temperature_id[k]`. Semantics depend on heater type: **room (air) temperature** for `thermal_config` loads (kept within `min_temps`/`max_temps` band toward `desired_temps`); **thermal-storage/tank temperature** for `thermal_battery` loads.|
55
+
|`heating_demand_heater{k}`|`_publish_thermal_loads`|**kWh**|**thermal energy** delivered to the storage (heat in), not electrical input | only set for `thermal_battery`-type loads (not bare `thermal_config`) | 1:1 |`energy`|`custom_heating_demand_id[k]`. The thermal-battery model carries a separate `heatpump_cops` parameter (`optimization.py:310`); electrical input = thermal / COP, so the two are decoupled. Unit `"kWh"` confirmed at `utils.py` (`heating_demand_friendly_name`). |
56
+
|`P_batt`|`_publish_battery_data`| W |positive = discharge (battery → house), negative = charge (house/grid → battery) |`optim_conf.set_use_battery = true`; row skipped if column missing | 1:1 |`batt`|`custom_batt_forecast_id`. Sum of `p_sto_pos + p_sto_neg` at `optimization.py:2312`. SOC reconstruction at lines 2319-2322 confirms direction: `power_flow = p_sto_pos / eff_dis + p_sto_neg * eff_chg`, then `SOC_opt = soc_init - cumulative_change / cap` (so positive `P_batt` drives SOC down → discharge).|
57
57
|`SOC_opt`|`_publish_battery_data`|**fraction (0..1) in CSV; ×100 in HA**| n/a (state) |`optim_conf.set_use_battery = true`|**×100**|`SOC`| See [SOC_opt scaling callout](#soc_opt-scaling-callout)|
58
-
|`P_grid`|`_publish_grid_and_costs`| W |(TBD — import vs export sign, see PR comment 5) | always | 1:1 |`power`|`custom_grid_forecast_id`|
58
+
|`P_grid`|`_publish_grid_and_costs`| W |positive = import (grid → house), negative = export (house → grid) | always | 1:1 |`power`|`custom_grid_forecast_id`. `P_grid = P_grid_pos + P_grid_neg` at `optimization.py:2299`. `p_grid_pos` is CVXPY `nonneg=True` bounded by `maximum_power_from_grid` (line 803); `p_grid_neg` is `nonpos=True` bounded by `-maximum_power_to_grid` (line 798).|
59
59
|`cost_fun_<name>`|`_publish_grid_and_costs`| € | n/a (cost) | always; multi-column (filter on substring `"cost_fun_"`) | 1:1 |`cost_fun`| Components depend on `costfun` (`profit`, `cost`, `self-consumption`). HA single entity `custom_cost_fun_id` aggregates them. |
60
60
|`optim_status`|`_publish_grid_and_costs`| text | n/a | always (defaulted to `"Optimal"` with WARN log if missing) | n/a |`optim_status`| CVXPY status strings: `Optimal`, `Infeasible`, `Unbounded`, etc. `device_class=""`, `unit_of_measurement=""`. |
61
61
|`unit_load_cost`|`_publish_grid_and_costs`| €/kWh | n/a (price) | always | 1:1 |`unit_load_cost`| per-timestep tariff series for load |
@@ -71,13 +71,19 @@ component the chosen `costfun` decomposes into).
71
71
> exported CSV must therefore multiply by 100 themselves if they expect percent;
72
72
> consumers reading the HA entity get percent already and must not double-scale.
73
73
74
-
## Sign conventions — open
74
+
## Sign conventions
75
75
76
-
Five power columns and two thermal columns have ambiguous sign conventions until
77
-
maintainer confirms. The opened PR carries those questions in its description; once
78
-
answered, this doc gets a follow-up patch on the same branch that replaces the `(TBD —
79
-
see PR comment N)` markers with the confirmed convention. Until then, consumers must
80
-
inspect the source or wait for the merged version.
76
+
All sign conventions are derived from `src/emhass/optimization.py` (the CVXPY model
77
+
definition). Per-column citations are inline in the column table above. Summary:
78
+
79
+
| Quantity | Positive means | Negative means |
80
+
|----------|----------------|----------------|
81
+
|`P_PV`| PV production (panels → DC bus) | (always ≥ 0; PV does not consume) |
@@ -120,4 +126,4 @@ DataFrame to the published columns before returning.
120
126
121
127
| Version | Date | Change |
122
128
|---------|------|--------|
123
-
| 1.0 | 2026-05-10 | Initial published schema. 7 sign conventions still TBD; will be resolved in a follow-up patch on the same PR branch before merge. |
129
+
| 1.0 | 2026-05-10 | Initial published schema. Sign conventions derived from `src/emhass/optimization.py` with inline citations. |
0 commit comments