Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ✨ **On a dynamic tariff, the cost sensors show the average price paid per bought kWh and the average price of all hours** (#1321).
- ✨ **The plan says where the battery ends tonight and tomorrow** (#1321), on its own trajectory and floor.
- ✨ **The night plan also respects the inverter's own end-of-discharge level, when SEM finds it** (#1358). Change it in the inverter's app; the plan follows.
- ✨ **A heat pump's COP can follow the outdoor temperature, so the plan picks the hours where heat is cheapest** (#1081).
- ✨ **With a heat meter, SEM learns the heat pump's real COP per outdoor temperature** (#1081). Off until you pick "Learned".

# [2.3.0-beta.14] — 13.11.2026

Expand Down
36 changes: 36 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ class _SEMYAMLModeSkip(Exception):
# #600 — load-device kWh energy counters (derive power when no power sensor);
# read at controller construction → reload on change.
"heat_pump_energy_sensor", "hot_water_energy_sensor",
# (#1081) the heat meter — set on the controller at construction
"heat_pump_heat_energy_sensor",
# #602 — heat pump / hot water rated power (W), read at controller
# construction; was config-flow-only, now settable from the dashboard.
"heat_pump_rated_power", "hot_water_rated_power",
Expand Down Expand Up @@ -1890,6 +1892,7 @@ def _heat_pump_rows(full_config: dict) -> list[dict]:
"heat_pump_relay1_entity", "heat_pump_relay2_entity",
"heat_pump_climate_entity", "heat_pump_power_sensor",
"heat_pump_energy_sensor", "heat_pump_temperature_sensor",
"heat_pump_heat_energy_sensor", # (#1081)
"heat_pump_rated_power", "heat_pump_priority",
"heat_pump_boost_offset", "heat_pump_max_setpoint",
"heat_pump_force_on_threshold", "heat_pump_cop",
Expand Down Expand Up @@ -3302,6 +3305,9 @@ def _deferred_entity_check(_now, _name=charger_name,
relay2_off_value=_row.get("heat_pump_relay2_off_value"),
step_down=_row.get("heat_pump_step_down") or STEP_DOWN_OFF,
)
# (#1081) the heat meter the COP is learned from (read only)
hp_extra.heat_energy_entity_id = (
_row.get("heat_pump_heat_energy_sensor") or None)
# (#1323) …and a step-down SEM left: re-owned from its stored
# memo, so the next cycle takes it back unless its cause stands.
_seed_step_down(coordinator, hp_extra)
Expand Down Expand Up @@ -5410,6 +5416,10 @@ async def async_update_device_config(call) -> None:
"tariff_rate", "tariff_entity", "tariff_meter",
# (#1341) the load's own name — display only
"name",
# (#1081) a heat pump's COP against the outdoor temperature
"cop_curve",
# (#1081) where its COP comes from: flat, curve or learned
"cop_source",
):
# (#559/#620) goal engine — persisted + applied live
registry = getattr(coordinator, "_device_registry", None)
Expand Down Expand Up @@ -5453,6 +5463,28 @@ async def async_update_device_config(call) -> None:
translation_key="invalid_device_property",
translation_placeholders={"property": f"{prop}={value}"},
)
# (#1081) 2–3 points "°C:COP", or empty to clear. A curve that
# cannot be read would be stored and then silently be no curve.
if prop == "cop_curve":
from .coordinator.heat_cost import (
format_cop_curve, parse_cop_curve)
_curve = parse_cop_curve(value)
if str(value).strip() and _curve is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device_property",
translation_placeholders={"property": f"{prop}={value}"},
)
value = format_cop_curve(_curve)
if prop == "cop_source":
from .coordinator.heat_cost import COP_SOURCES
value = str(value or "").strip().lower()
if value and value not in COP_SOURCES:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device_property",
translation_placeholders={"property": f"{prop}={value}"},
)
if prop == "top_up_policy" and str(value) not in (
"solar_only", "cheap_hours"
):
Expand Down Expand Up @@ -5584,6 +5616,10 @@ async def async_update_device_config(call) -> None:
"hvac_mode",
# (#1341) the load's own name
"name",
# (#1081) a heat pump's COP curve, "-7:2.5, 2:3.2, 12:4.2"
"cop_curve",
# (#1081) flat / curve / learned ("" = as before)
"cop_source",
]),
vol.Required("value"): cv.string,
}),
Expand Down
14 changes: 14 additions & 0 deletions config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1799,6 +1799,7 @@ def shown(key: str) -> Any:
"heat_pump_tariff_rate", "heat_pump_tariff_entity", "heat_pump_tariff_meter",
"heat_pump_temperature_sensor",
"heat_pump_energy_sensor",
"heat_pump_heat_energy_sensor", # (#1081) the heat meter
"heat_pump_rated_power",
"heat_pump_force_on_threshold",
"heat_pumps",
Expand Down Expand Up @@ -3996,6 +3997,13 @@ def _opt(key):
"heat_pump_power_sensor",
description={"suggested_value": _opt("heat_pump_power_sensor")},
): _power_sensor_selector(getattr(self, "hass", None)),
# (#1081) the heat meter: SEM learns the pump's COP from it
vol.Optional(
"heat_pump_heat_energy_sensor",
description={"suggested_value": _opt("heat_pump_heat_energy_sensor")},
): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor", device_class="energy")
),
vol.Optional(
"heat_pump_boost_offset",
default=_c("heat_pump_boost_offset", 2.0),
Expand Down Expand Up @@ -4214,6 +4222,12 @@ def _row(key, fb=None):
): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor", device_class="energy")
),
vol.Optional(
"heat_pump_heat_energy_sensor",
description={"suggested_value": _row("heat_pump_heat_energy_sensor")},
): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor", device_class="energy")
),
vol.Optional(
"heat_pump_temperature_sensor",
description={"suggested_value": _row("heat_pump_temperature_sensor")},
Expand Down
14 changes: 11 additions & 3 deletions consts/integration_roster.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
SCHEMA: Final = 1


ROSTER_META: Final[Dict[str, Any]] = {'generated_at': '2026-11-13T19:39:56Z', 'sources': {'core_analytics': 'https://analytics.home-assistant.io/data.json', 'core_index': 'https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/generated/integrations.json', 'custom_installs': 'https://analytics.home-assistant.io/custom_integrations.json', 'hacs': 'https://data-v2.hacs.xyz/integration/data.json', 'website': 'https://www.home-assistant.io/integrations.json'}, 'install_floor': 50, 'candidates': 1365, 'kept': 203, 'with_roles': 70, 'roles_mined': 204}
ROSTER_META: Final[Dict[str, Any]] = {'generated_at': '2026-11-13T22:43:16Z', 'sources': {'core_analytics': 'https://analytics.home-assistant.io/data.json', 'core_index': 'https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/generated/integrations.json', 'custom_installs': 'https://analytics.home-assistant.io/custom_integrations.json', 'hacs': 'https://data-v2.hacs.xyz/integration/data.json', 'website': 'https://www.home-assistant.io/integrations.json'}, 'install_floor': 50, 'candidates': 1365, 'kept': 203, 'with_roles': 72, 'roles_mined': 208}

#: domain -> what the ecosystem says this integration is.
ROSTER: Final[Dict[str, Dict[str, Any]]] = {
Expand Down Expand Up @@ -116,7 +116,7 @@
'keba': {'name': 'Keba Charging Station', 'repo': 'home-assistant/core', 'origin': 'core', 'installs': 427, 'kind': 'charger', 'kind_from': 'vocabulary'},
'kia_uvo': {'name': 'Kia Uvo / Hyundai Bluelink', 'repo': 'Hyundai-Kia-Connect/kia_uvo', 'origin': 'hacs', 'installs': 5987, 'kind': 'appliance', 'kind_from': 'keyword'},
'kostal_plenticore': {'name': 'Kostal Plenticore Solar Inverter', 'repo': 'home-assistant/core', 'origin': 'core', 'installs': 2309, 'kind': 'energy', 'kind_from': 'keyword'},
'lambda_heat_pumps': {'name': 'Lambda Heat Pumps', 'repo': 'GuidoJeuken-6512/lambda_heat_pumps', 'origin': 'hacs', 'installs': 276, 'kind': 'energy', 'kind_from': 'vocabulary_name_only'},
'lambda_heat_pumps': {'name': 'Lambda Heat Pumps', 'repo': 'GuidoJeuken-6512/lambda_heat_pumps', 'origin': 'hacs', 'installs': 276, 'kind': 'energy', 'kind_from': 'vocabulary'},
'lektrico': {'name': 'Lektrico Charging Station', 'repo': 'home-assistant/core', 'origin': 'core', 'installs': 159, 'kind': 'other', 'kind_from': 'keyword'},
'leneda': {'name': 'Leneda', 'repo': 'koosoli/Leneda-integration', 'origin': 'hacs', 'installs': 58, 'kind': 'other', 'kind_from': 'keyword'},
'luxtronik2': {'name': 'Luxtronik', 'repo': 'BenPru/luxtronik', 'origin': 'hacs', 'installs': 614, 'kind': 'energy', 'kind_from': 'vocabulary_name_only'},
Expand Down Expand Up @@ -198,7 +198,7 @@
'solstice_season': {'name': 'Solstice Season', 'repo': 'moerk-o/SolsticeHub', 'origin': 'hacs', 'installs': 77, 'kind': 'energy', 'kind_from': 'keyword'},
'stiebel_eltron': {'name': 'STIEBEL ELTRON', 'repo': 'home-assistant/core', 'origin': 'core', 'installs': 152, 'kind': 'energy', 'kind_from': 'keyword'},
'stiebel_eltron_http': {'name': 'Stiebel Eltron', 'repo': 'pmq/stiebel-eltron-http', 'origin': 'hacs', 'installs': 59, 'kind': 'energy', 'kind_from': 'keyword'},
'stiebel_eltron_isg': {'name': 'Stiebel Eltron ISG', 'repo': 'pail23/stiebel_eltron_isg_component', 'origin': 'hacs', 'installs': 816, 'kind': 'energy', 'kind_from': 'vocabulary_name_only'},
'stiebel_eltron_isg': {'name': 'Stiebel Eltron ISG', 'repo': 'pail23/stiebel_eltron_isg_component', 'origin': 'hacs', 'installs': 816, 'kind': 'energy', 'kind_from': 'vocabulary'},
'stromligning': {'name': 'Strømligning', 'repo': 'MTrab/stromligning', 'origin': 'hacs', 'installs': 575, 'kind': 'energy', 'kind_from': 'keyword'},
'subaru': {'name': 'Subaru', 'repo': 'home-assistant/core', 'origin': 'core', 'installs': 556, 'kind': 'vehicle', 'kind_from': 'vocabulary'},
'sungrow': {'name': 'Sungrow iSolarCloud', 'repo': 'KRoperUK/sungrow-hass', 'origin': 'hacs', 'installs': 595, 'kind': 'energy', 'kind_from': 'vocabulary'},
Expand Down Expand Up @@ -363,10 +363,14 @@
},
'idm_heatpump': {
'battery_soc': {'platform': 'sensor', 'keys': ('battery_soc',), 'options': ()},
'heat_pump_heat_energy': {'platform': 'sensor', 'keys': ('total_heat_energy',), 'options': ()},
},
'keba': {
'ev_current_control': {'platform': 'service', 'keys': ('keba.set_current',), 'options': (), 'services': {'keba.set_current': {'fields': ('current',), 'target': None}}},
},
'lambda_heat_pumps': {
'heat_pump_heat_energy': {'platform': 'sensor', 'keys': ('compressor_thermal_energy_output_accumulated', 'heating_thermal_energy_total'), 'options': ()},
},
'marstek_venus_energy_manager': {
'battery_charge_end_soc': {'platform': 'number', 'keys': ('charging_cutoff_capacity',), 'options': ()},
'battery_charge_limit': {'platform': 'number', 'keys': ('max_charge_power', 'system_max_charge_power'), 'options': (), 'exact_only': ('max_charge_power',)},
Expand Down Expand Up @@ -452,6 +456,7 @@
'solar_power': {'platform': 'sensor', 'keys': ('solar_power',), 'options': ()},
},
'solarfocus': {
'heat_pump_heat_energy': {'platform': 'sensor', 'keys': ('hp_thermal_energy_heating', 'hp_thermal_energy_total'), 'options': ()},
'solar_power': {'platform': 'sensor', 'keys': ('pv_power',), 'options': ()},
},
'solarman': {
Expand All @@ -469,6 +474,9 @@
'battery_soc': {'platform': 'sensor', 'keys': ('battery_soc',), 'options': ()},
'grid_power': {'platform': 'sensor', 'keys': ('grid_active_power',), 'options': ()},
},
'stiebel_eltron_isg': {
'heat_pump_heat_energy': {'platform': 'sensor', 'keys': ('produced_heating', 'produced_heating_total'), 'options': ()},
},
'subaru': {
'vehicle_range': {'platform': 'sensor', 'keys': ('ev_range', 'range'), 'options': (), 'exact_only': ('range',)},
'vehicle_soc': {'platform': 'sensor', 'keys': ('ev_battery_level',), 'options': ()},
Expand Down
25 changes: 25 additions & 0 deletions consts/role_lexicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,26 @@
"not": (r"average", r"level", r"rank", r"rating", r"base", r"trend",
r"phase", r"next", r"last", r"min", r"max", r"percent"),
},
# (#1081) The heat a heat pump has DELIVERED, as a rising kWh counter —
# its own heat meter. With the electricity it used, SEM learns the
# pump's real COP at each outdoor temperature. Only a lifetime total
# counts: a daily/monthly one resets, and hot water, cooling or defrost
# heat is not the space heat the curve is for. SEM only reads it.
"heat_pump_heat_energy": {
"platform": "sensor",
"any": (r"thermal_energy", r"(?:^|_)heat_energy",
r"produced_heat(ing)?", r"heat_meter", r"heat_quantity",
r"heat_output_energy", r"thermal_energy_output"),
"not": (r"daily", r"today", r"month", r"year", r"hour", r"week",
r"consum", r"electric", r"input", r"cool", r"defrost",
r"drinking", r"hot_water", r"dhw", r"tapping", r"water_heat",
r"circulation", r"flow", r"current_yield", r"power", r"cop",
r"temp", r"pool",
# a counter that is not the lifetime one, or not the pump's
r"yesterday", r"last", r"since", r"reset", r"session",
r"cycle", r"solar", r"boiler", r"biomass", r"^bb_",
r"pellet", r"wood", r"gas"),
},
"battery_power": {
"platform": "sensor",
"any": (r"^battery_(active_)?power$", r"^storage_.*charge_discharge_power$",
Expand Down Expand Up @@ -575,6 +595,8 @@ def count_said(words: str, markers) -> int:
# confirmed proposal is
"grid_import_power": "grid_import_power_entity",
"grid_export_power": "grid_export_power_entity",
# (#1081) the primary heat pump's heat meter — read only
"heat_pump_heat_energy": "heat_pump_heat_energy_sensor",
}

#: Roles that only mean anything TOGETHER. A brand that declares one half of
Expand Down Expand Up @@ -649,6 +671,8 @@ def count_said(words: str, markers) -> int:
"power_or_percent": frozenset({"W", "kW", "%"}),
"percent": frozenset({"%"}),
"current": frozenset({"A"}),
# (#1081) a counter of energy, any size
"energy": frozenset({"Wh", "kWh", "MWh"}),
}
ROLE_EXPECTED_UNIT: Final[Dict[str, str]] = {
"battery_charge_limit": "power",
Expand All @@ -662,6 +686,7 @@ def count_said(words: str, markers) -> int:
"grid_power": "power",
"grid_import_power": "power",
"grid_export_power": "power",
"heat_pump_heat_energy": "energy", # (#1081)
"ev_current_control": "current",
"vehicle_soc": "percent",
}
Expand Down
Loading
Loading