Skip to content

Refactor/drop grid charge margin pct - #505

Merged
ffunes merged 28 commits into
release/v1.5.0from
refactor/drop-grid-charge-margin-pct
Sep 21, 2026
Merged

ffunes merged 28 commits into
release/v1.5.0from
refactor/drop-grid-charge-margin-pct

Conversation

@ffunes

@ffunes ffunes commented Sep 21, 2026

Copy link
Copy Markdown
Owner

What

Approved roadmap item

Discussion / issue: #

Tests

ffunes and others added 28 commits September 19, 2026 11:00
"Maximum contracted power" only limited charging. An excluded device is
subtracted from the grid reading by design, so the PD controller never
covers it and the whole load goes to the meter the breaker watches.

_apply_icp_excluded_protection projects the physical grid with the same
marginal model as the ICP charge clamp (the PD converges sensor_actual to
the target, so physical = target + hidden) and hands back just the excess
above the contracted power, clamped to the still-hidden excluded share so
a peak-shaving add-back is not counted twice. Always on, no new setting.

Scope is excluded devices only: cases gated by a discharge blocker (EV
charger without telemetry, price blocks, time slots, manual mode) stay as
they were.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)

Vende energia reservada para consumo solo cuando cada kWh queda ligado 1:1 a
un deficit posterior recomprable y el precio de exportacion supera
estrictamente el maximo precio de importacion posterior mas el coste
adicional. Todo lo incierto resuelve a "no vender".

El horizonte protegido llega inyectado (PricingManager.energy_horizon_end);
el planner no busca el cruce solar. Sin dependencia de Home Assistant.

Trigger 1 queda fuera: depende del coste de la energia almacenada (#269), aun
sin implementar. Tambien fuera el lease/TTL por driver (RF-051): la
exportacion deliberada ya funciona hoy via set_setpoint_override sin lease.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adaptador del planner puro: construye el horizonte, cachea el plan (300 s) y
convierte la asignación vigente en un setpoint override.

- Frontera tz-aware en el módulo de runtime: la capa de precios sigue naive
  local y `_aware()` reengancha la zona solo al cruzar al planner.
- Prioridad 4, por debajo de los dos overrides de curtailment: el riesgo
  manda sobre la oportunidad.
- Un único enganche síncrono al final de `_refresh_operation_blockers`, con
  los blockers ya reconstruidos.
- Reutiliza `consumption_by_slot`, `_profile_remaining_consumption` y
  `distribute_solar_forecast`; cualquier dato ausente retira el override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Flujo de configuración y de opciones: activación, potencia máxima neta y
  coste adicional, en la sección de precios dinámicos.
- Dos números declarativos en `CONFIG_NUMBER_DEFINITIONS` (presence-gated por
  la clave de activación, como sus hermanos) y sensor de diagnóstico
  `high_price_discharge_status` con el estado operativo y el plan.
- Volcado de `get_status()` en diagnostics.
- Paridad de traducciones: los 3 campos del flujo y las 3 entidades nuevas,
  con los seis estados traducidos, en strings.json y los 6 idiomas.
- Panel: etiqueta en los 6 mapas I18N, fila de diagnóstico, allowlist
  `items:` y tonos por estado; el help sigue el patrón en-only del fichero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La clave de activación se backfillea en el setup, así que las entradas
anteriores a la función también obtienen sus entidades sin reabrir el flujo
de opciones. Con el interruptor en el sistema (apagado por defecto) la
función se activa y se configura desde el dashboard; al apagarlo el override
cae en el siguiente ciclo por la puerta de ámbito, sin liberación explícita.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…de arbitraje (#270)

`high_price_discharge_additional_cost` y `min_arbitrage_margin` preguntan lo
mismo — ¿compensa el diferencial el viaje de ida y vuelta? — solo que desde
lados opuestos de la operación. Dos campos por kWh únicamente permitían que las
dos respuestas se contradijeran, así que se borra el propio y el runtime lee el
que la carga ya usaba. Sin publicar, sin migración.

Fuera: la clave, su número, los campos de los dos flujos, la fila del panel con
sus 6 etiquetas y su ayuda, y 5 entradas en cada uno de los 7 JSON. La ayuda de
`min_arbitrage_margin` y su sección de documentación dicen ahora que rige las
dos direcciones, y el diagnóstico nombra la clave que de verdad lee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rada (#270)

`async_unload_entry` limpiaba explícitamente el runtime de anti-vertido y el de
precio negativo, y dejaba este puesto. En la práctica las escrituras de apagado
paran las baterías, pero RF-036 y el criterio 13 exigen la retirada explícita y
la convención está escrita en ese mismo comentario.

La retirada normal viaja en el siguiente `refresh_override`; en unload no hay
siguiente ciclo, así que `clear_runtime` reutiliza `_release` con el nombre y la
forma de sus hermanos del gestor de precios.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La funcion tenia entidades y diagnosticos pero ningun control: el flag
solo vivia en el flujo de opciones, su clave no estaba en el backfill de
setup y el slider de ahorro minimo no estaba en la allowlist del panel.

Anade el switch (presence-gated, como el de descarga por precio alto),
las dos filas del panel y el nombre del switch en los 7 JSON. El binary
sensor pasa a "... Status" para no compartir nombre con el switch, como
ya hace la predescarga inteligente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unica funcion de precios sin documentacion de usuario: la retencion de
excedente y el margen de arbitraje ya la tienen. Nueva subseccion en
Price-based discharge control con la regla de recompra, los dos
controles, los guards y el sensor de estado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cto (#270)

DEFAULT_HIGH_PRICE_DISCHARGE_MAX_POWER = 0.0 combinado con el rechazo de
_config() ante potencia <= 0 dejaba la funcion muerta en silencio cuando el
usuario activaba el interruptor sin tocar el deslizador. El valor por
defecto ahora es la potencia de descarga de la flota
(default_high_price_discharge_max_power en const/integration_const.py),
resuelto en los tres sitios donde antes se leia la constante estatica:
el controlador (__init__.py, arranque y hot-reload), el config/options
flow y number.py (valor mostrado en el panel).

Un valor de 0 W puesto a mano por el usuario, o el caso de una flota sin
baterias configuradas, se siguen respetando: solo cambia el fallback
cuando la clave esta ausente de config_entry.data. El estado
STATE_INVALID_CONFIGURATION / GUARD_NO_MAX_POWER ya existente cubre el
caso legible que pedia la ficha, sin cambios de logica en
control/high_price_discharge.py mas alla de documentar el origen del
valor.

Actualiza el texto de ayuda del deslizador (ya no dice "0 W deja la
funcion inactiva") en strings.json + las 6 traducciones + SYS_HELP del
panel.

Test: tests/test_high_price_discharge_default_power.py

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…el panel (#270)

La reserva de descarga por precio traia diagnostico (discharge_reserve_status)
pero ningun control fuera del asistente de configuracion: era la unica de las
seis funciones economicas de precio dinamico sin superficie en el dashboard.

Se anade DischargeReserveSwitch (gemelo literal de SurplusPriceHoldSwitch) y
la entrada de discharge_reserve_min_saving en CONFIG_NUMBER_DEFINITIONS,
condicionada a la presencia (no al valor) de discharge_reserve_enabled, igual
que su gemela surplus_hold_min_saving. Backfill del flag en entradas
existentes para que el interruptor aparezca sin reabrir el asistente.

El sensor de estado se renombra a "... Status" en las 7 traducciones para no
compartir nombre con el nuevo interruptor, siguiendo el mismo patron ya
aplicado a surplus_price_hold/surplus_price_hold_status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…redictiva

La tarjeta de carga predictiva mostraba 19 filas de golpe. Los items marcados
`adv: true` en SYS_SECTIONS ahora se ocultan con display:none (mismo mecanismo
que ya usa `gate` para las filas hijas de un interruptor) salvo que el usuario
active "Ajustes avanzados" en la barra de reordenar; el estado se guarda en
localStorage por navegador, sin entidad de HA. El mecanismo es generico:
cualquier item de cualquier seccion puede marcarse `adv`, y una seccion que se
quede sin filas visibles no dibuja su cabecera (misma regla que ya aplicaba
cuando una seccion no tiene entidades vivas en el registro).

Se marcan como avanzados los 13 deslizadores/umbrales de la carga predictiva
por precio mas el par de T2 (solo el numero discharge_reserve_min_saving; el
switch discharge_reserve queda visible, igual que el resto de interruptores
de funcion).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Los modos Precio Dinamico y Precio en Tiempo Real son excluyentes, asi que
dp_price_discharge_control y rt_price_discharge_control eran la misma clase con
dos translation_key, dos unique_id y dos entity_id para el mismo comportamiento.

Ahora hay una sola entidad price_discharge_control. El controlador expone
dp_/rt_price_discharge_control como propiedades de solo lectura que la aliasan,
de modo que pricing/engine.py -que lee la que corresponde al modo activo- no se
toca.

La migracion v12 -> v13 funde el valor de ambas claves (gana la del modo activo)
y re-clava en el registro la entidad que corresponde al modo activo sobre el
unique_id nuevo, dejando su entity_id -y con el su historial- intacto. Si un
cambio de modo anterior dejo las dos entidades, la del modo inactivo se borra en
vez de quedar huerfana: las huerfanas ya se han renderizado como controles
duplicados en el panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…útil

El margen porcentual inflaba el déficit ya calculado, así que cubría más
cuanto menos sol había previsto: un seguro grande en días nublados y
diminuto en los días soleados en los que una previsión optimista sale
cara. El margen en kWh actúa donde está el riesgo (recorta la propia
previsión solar) y además lo usan el anti-vertido, la absorción de
excedente y la línea de tiempo solar.

- Borrado predictive_grid_charge_margin_pct: entidad, clave de config,
  los dos pasos del flujo y el parámetro de
  calculate_planned_grid_charge_kwh. Se pierde a propósito el inflado de
  floor_required, que es un objetivo de SOC y no una previsión.
- Migración v13 -> v14: quita la clave y borra la entidad del registro
  en vez de dejarla huérfana. Un margen puesto a mano no se reescribe.
- predictive_safety_margin_kwh deja de tener 0 por defecto: ahora es el
  5% de la capacidad de la flota, así que significa lo mismo en una
  instalación de 5 kWh que en una de 30. Solo para altas nuevas.
- Corregido su texto de ayuda en los 6 idiomas y en el panel: decía que
  se sumaba a la previsión de consumo, y lo que hace es restarse de la
  previsión solar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ores

Cada umbral, interruptor y limite que duplicaban ya es una entidad viva del
panel escribiendo las mismas claves de entry.data, asi que Precios Dinamicos
pasa de veintiun campos a cinco. Lo que queda es lo que una entidad no puede
hacer: validar contra hass.states y recargar plataformas.

Los textos de ayuda que vivian en el formulario se trasladan a SYS_HELP del
panel en los seis idiomas, para que el ajuste siga explicandose donde ahora
se toca.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…270)

"Limite de exportacion por precio alto" y "Limite de exportacion de la
predescarga" pedian lo mismo que el limite de descarga del sistema, que ya
los acota: un deslizador por funcion solo podia contradecirlo. Ahora ambas
usan la potencia de descarga de la flota y quien quiera exportar menos baja
ese limite (o lo gobierna desde una automatizacion).

La reserva de SOC de la predescarga pasa a 20% por defecto: con 0% el
anticurtailment podia vaciar la flota hasta mi minimo de cada bateria por una
prevision solar que puede no llegar.

Migracion v14 -> v15: borra las dos claves, elimina sus entidades del
registro y suelta la reserva 0% de las instalaciones que nunca activaron la
predescarga, para que alcancen el nuevo defecto.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…inamicos

Descarga por precio alto, retencion de excedente por precio y reserva de
descarga abortan si el modo predictivo no es Precios Dinamicos, pero sus
claves se rellenan en toda entrada, asi que la presencia no dice nada del
modo activo: en Franjas Horarias y Precio en Tiempo Real aparecian como
interruptores, deslizadores y filas de estado muertos en el panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… cada tarjeta

Era un boton global de la barra de organizar que reconstruia toda la pestana
y escondia la tarjeta entera cuando todas sus filas eran avanzadas. Ahora
cada tarjeta con filas `adv` lleva el suyo, con estado propio: las filas se
quedan en el DOM y una clase `adv-off` las oculta por CSS, asi que alternar
no recrea los deslizadores de las demas tarjetas bajo el raton.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cada item de diagPredictive lleva una etiqueta `grp` y el renderizador
dibuja una linea divisoria cuando cambia entre dos filas vivas: base,
precios de carga, predescarga, descarga por precio, estado y accion.
El separador se oculta con la funcion apagada (gatedNodes) y se marca
como avanzado cuando todo el grupo que abre lo es.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Las cuatro filas de estado estaban juntas al final; ahora cada una cierra
el grupo de su funcion (interruptor + sliders + estado) y el antiguo grupo
"export" se divide en excedente, precio alto y reserva.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El conmutador que cambia lo que muestra la tarjeta encabeza el grupo de
iconos; el margen automatico pasa a el y la separacion se recalcula desde
el nuevo orden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ecto

El controlador caía al 5% de la capacidad de la flota cuando la clave no
estaba en config, pero la entidad number seguía mostrando el "default"
autorizado (0.0). En un alta nueva el panel decía 0.0 kWh mientras el
motor recortaba la previsión con otra cifra: un valor oculto.

- "default" admite un callable y native_value lo resuelve, igual que
  dynamic_bounds hace con min/max.
- Un margen ya guardado (incluido un 0.0 explícito) sigue ganando: el
  defecto solo entra si la clave no está, así que ninguna configuración
  existente se reescribe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187cf94 anadio la migracion v14 -> v15 pero dejo el `return` anticipado en
`entry.version >= 14`: una instalacion ya en v14 salia antes de ejecutarla, se
quedaba con las claves de potencia de exportacion y nunca llegaba a v15. Solo
migraban las entradas por debajo de v14.

Los dos tests de flujo HA seguian afirmando la version 14 (de ahi el fallo de
pytest en el PR); el noop de la guarda ahora lee `MarstekVenusConfigFlow.VERSION`
en vez de un numero fijo, que es lo que dejo pasar el despiste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ffunes
ffunes merged commit 6a88ddf into release/v1.5.0 Sep 21, 2026
3 checks passed
ffunes added a commit that referenced this pull request Sep 21, 2026
…into release/v1.5.0

Los commits locales de #270/#505 llegaron aguas arriba aplastados en 6a88ddf,
con commits posteriores del PR encima. El arbol resultante es el de MERGE_HEAD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ffunes added a commit that referenced this pull request Sep 22, 2026
* feat(icp): cover excluded-device load above the contracted power

"Maximum contracted power" only limited charging. An excluded device is
subtracted from the grid reading by design, so the PD controller never
covers it and the whole load goes to the meter the breaker watches.

_apply_icp_excluded_protection projects the physical grid with the same
marginal model as the ICP charge clamp (the PD converges sensor_actual to
the target, so physical = target + hidden) and hands back just the excess
above the contracted power, clamped to the still-hidden excluded share so
a peak-shaving add-back is not counted twice. Always on, no new setting.

Scope is excluded devices only: cases gated by a discharge blocker (EV
charger without telemetry, price blocks, time slots, manual mode) stay as
they were.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changelog): note the excluded-device contracted-power guard

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: update changelog

* feat(pricing): planner puro de descarga por precio alto (trigger 2, #270)

Vende energia reservada para consumo solo cuando cada kWh queda ligado 1:1 a
un deficit posterior recomprable y el precio de exportacion supera
estrictamente el maximo precio de importacion posterior mas el coste
adicional. Todo lo incierto resuelve a "no vender".

El horizonte protegido llega inyectado (PricingManager.energy_horizon_end);
el planner no busca el cruce solar. Sin dependencia de Home Assistant.

Trigger 1 queda fuera: depende del coste de la energia almacenada (#269), aun
sin implementar. Tambien fuera el lease/TTL por driver (RF-051): la
exportacion deliberada ya funciona hoy via set_setpoint_override sin lease.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(pricing): runtime de descarga por precios altos (trigger 2, #270)

Adaptador del planner puro: construye el horizonte, cachea el plan (300 s) y
convierte la asignación vigente en un setpoint override.

- Frontera tz-aware en el módulo de runtime: la capa de precios sigue naive
  local y `_aware()` reengancha la zona solo al cruzar al planner.
- Prioridad 4, por debajo de los dos overrides de curtailment: el riesgo
  manda sobre la oportunidad.
- Un único enganche síncrono al final de `_refresh_operation_blockers`, con
  los blockers ya reconstruidos.
- Reutiliza `consumption_by_slot`, `_profile_remaining_consumption` y
  `distribute_solar_forecast`; cualquier dato ausente retira el override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(pricing): superficie de la descarga por precios altos (#270)

- Flujo de configuración y de opciones: activación, potencia máxima neta y
  coste adicional, en la sección de precios dinámicos.
- Dos números declarativos en `CONFIG_NUMBER_DEFINITIONS` (presence-gated por
  la clave de activación, como sus hermanos) y sensor de diagnóstico
  `high_price_discharge_status` con el estado operativo y el plan.
- Volcado de `get_status()` en diagnostics.
- Paridad de traducciones: los 3 campos del flujo y las 3 entidades nuevas,
  con los seis estados traducidos, en strings.json y los 6 idiomas.
- Panel: etiqueta en los 6 mapas I18N, fila de diagnóstico, allowlist
  `items:` y tonos por estado; el help sigue el patrón en-only del fichero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(pricing): toggle de descarga por precios altos en el panel (#270)

La clave de activación se backfillea en el setup, así que las entradas
anteriores a la función también obtienen sus entidades sin reabrir el flujo
de opciones. Con el interruptor en el sistema (apagado por defecto) la
función se activa y se configura desde el dashboard; al apagarlo el override
cae en el siguiente ciclo por la puerta de ámbito, sin liberación explícita.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(pricing): la descarga por precios altos reutiliza el margen de arbitraje (#270)

`high_price_discharge_additional_cost` y `min_arbitrage_margin` preguntan lo
mismo — ¿compensa el diferencial el viaje de ida y vuelta? — solo que desde
lados opuestos de la operación. Dos campos por kWh únicamente permitían que las
dos respuestas se contradijeran, así que se borra el propio y el runtime lee el
que la carga ya usaba. Sin publicar, sin migración.

Fuera: la clave, su número, los campos de los dos flujos, la fila del panel con
sus 6 etiquetas y su ayuda, y 5 entradas en cada uno de los 7 JSON. La ayuda de
`min_arbitrage_margin` y su sección de documentación dicen ahora que rige las
dos direcciones, y el diagnóstico nombra la clave que de verdad lee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(pricing): soltar el override de precios altos al descargar la entrada (#270)

`async_unload_entry` limpiaba explícitamente el runtime de anti-vertido y el de
precio negativo, y dejaba este puesto. En la práctica las escrituras de apagado
paran las baterías, pero RF-036 y el criterio 13 exigen la retirada explícita y
la convención está escrita en ese mismo comentario.

La retirada normal viaja en el siguiente `refresh_override`; en unload no hay
siguiente ciclo, así que `clear_runtime` reutiliza `_release` con el nombre y la
forma de sus hermanos del gestor de precios.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(pricing): superficie de la retencion de excedente por precio

La funcion tenia entidades y diagnosticos pero ningun control: el flag
solo vivia en el flujo de opciones, su clave no estaba en el backfill de
setup y el slider de ahorro minimo no estaba en la allowlist del panel.

Anade el switch (presence-gated, como el de descarga por precio alto),
las dos filas del panel y el nombre del switch en los 7 JSON. El binary
sensor pasa a "... Status" para no compartir nombre con el switch, como
ya hace la predescarga inteligente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(pricing): documentar la descarga por precio alto (#270, RF-049)

Unica funcion de precios sin documentacion de usuario: la retencion de
excedente y el margen de arbitraje ya la tienen. Nueva subseccion en
Price-based discharge control con la regla de recompra, los dos
controles, los guards y el sensor de estado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(pricing): la descarga por precio alto ya no muere en 0 W por defecto (#270)

DEFAULT_HIGH_PRICE_DISCHARGE_MAX_POWER = 0.0 combinado con el rechazo de
_config() ante potencia <= 0 dejaba la funcion muerta en silencio cuando el
usuario activaba el interruptor sin tocar el deslizador. El valor por
defecto ahora es la potencia de descarga de la flota
(default_high_price_discharge_max_power en const/integration_const.py),
resuelto en los tres sitios donde antes se leia la constante estatica:
el controlador (__init__.py, arranque y hot-reload), el config/options
flow y number.py (valor mostrado en el panel).

Un valor de 0 W puesto a mano por el usuario, o el caso de una flota sin
baterias configuradas, se siguen respetando: solo cambia el fallback
cuando la clave esta ausente de config_entry.data. El estado
STATE_INVALID_CONFIGURATION / GUARD_NO_MAX_POWER ya existente cubre el
caso legible que pedia la ficha, sin cambios de logica en
control/high_price_discharge.py mas alla de documentar el origen del
valor.

Actualiza el texto de ayuda del deslizador (ya no dice "0 W deja la
funcion inactiva") en strings.json + las 6 traducciones + SYS_HELP del
panel.

Test: tests/test_high_price_discharge_default_power.py

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(pricing): interruptor y deslizador de la reserva de descarga en el panel (#270)

La reserva de descarga por precio traia diagnostico (discharge_reserve_status)
pero ningun control fuera del asistente de configuracion: era la unica de las
seis funciones economicas de precio dinamico sin superficie en el dashboard.

Se anade DischargeReserveSwitch (gemelo literal de SurplusPriceHoldSwitch) y
la entrada de discharge_reserve_min_saving en CONFIG_NUMBER_DEFINITIONS,
condicionada a la presencia (no al valor) de discharge_reserve_enabled, igual
que su gemela surplus_hold_min_saving. Backfill del flag en entradas
existentes para que el interruptor aparezca sin reabrir el asistente.

El sensor de estado se renombra a "... Status" en las 7 traducciones para no
compartir nombre con el nuevo interruptor, siguiendo el mismo patron ya
aplicado a surplus_price_hold/surplus_price_hold_status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(panel): conmutador de Ajustes avanzados en la tarjeta de carga predictiva

La tarjeta de carga predictiva mostraba 19 filas de golpe. Los items marcados
`adv: true` en SYS_SECTIONS ahora se ocultan con display:none (mismo mecanismo
que ya usa `gate` para las filas hijas de un interruptor) salvo que el usuario
active "Ajustes avanzados" en la barra de reordenar; el estado se guarda en
localStorage por navegador, sin entidad de HA. El mecanismo es generico:
cualquier item de cualquier seccion puede marcarse `adv`, y una seccion que se
quede sin filas visibles no dibuja su cabecera (misma regla que ya aplicaba
cuando una seccion no tiene entidades vivas en el registro).

Se marcan como avanzados los 13 deslizadores/umbrales de la carga predictiva
por precio mas el par de T2 (solo el numero discharge_reserve_min_saving; el
switch discharge_reserve queda visible, igual que el resto de interruptores
de funcion).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(pricing): un solo interruptor de descarga segun precio (#270)

Los modos Precio Dinamico y Precio en Tiempo Real son excluyentes, asi que
dp_price_discharge_control y rt_price_discharge_control eran la misma clase con
dos translation_key, dos unique_id y dos entity_id para el mismo comportamiento.

Ahora hay una sola entidad price_discharge_control. El controlador expone
dp_/rt_price_discharge_control como propiedades de solo lectura que la aliasan,
de modo que pricing/engine.py -que lee la que corresponde al modo activo- no se
toca.

La migracion v12 -> v13 funde el valor de ambas claves (gana la del modo activo)
y re-clava en el registro la entidad que corresponde al modo activo sobre el
unique_id nuevo, dejando su entity_id -y con el su historial- intacto. Si un
cambio de modo anterior dejo las dos entidades, la del modo inactivo se borra en
vez de quedar huerfana: las huerfanas ya se han renderizado como controles
duplicados en el panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(predictive): un solo margen de previsión solar, con defecto útil

El margen porcentual inflaba el déficit ya calculado, así que cubría más
cuanto menos sol había previsto: un seguro grande en días nublados y
diminuto en los días soleados en los que una previsión optimista sale
cara. El margen en kWh actúa donde está el riesgo (recorta la propia
previsión solar) y además lo usan el anti-vertido, la absorción de
excedente y la línea de tiempo solar.

- Borrado predictive_grid_charge_margin_pct: entidad, clave de config,
  los dos pasos del flujo y el parámetro de
  calculate_planned_grid_charge_kwh. Se pierde a propósito el inflado de
  floor_required, que es un objetivo de SOC y no una previsión.
- Migración v13 -> v14: quita la clave y borra la entidad del registro
  en vez de dejarla huérfana. Un margen puesto a mano no se reescribe.
- predictive_safety_margin_kwh deja de tener 0 por defecto: ahora es el
  5% de la capacidad de la flota, así que significa lo mismo en una
  instalación de 5 kWh que en una de 30. Solo para altas nuevas.
- Corregido su texto de ayuda en los 6 idiomas y en el panel: decía que
  se sumaba a la previsión de consumo, y lo que hace es restarse de la
  previsión solar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(config): los formularios de carga predictiva solo piden sensores

Cada umbral, interruptor y limite que duplicaban ya es una entidad viva del
panel escribiendo las mismas claves de entry.data, asi que Precios Dinamicos
pasa de veintiun campos a cinco. Lo que queda es lo que una entidad no puede
hacer: validar contra hass.states y recargar plataformas.

Los textos de ayuda que vivian en el formulario se trasladan a SYS_HELP del
panel en los seis idiomas, para que el ajuste siga explicandose donde ahora
se toca.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(pricing): fuera los deslizadores de potencia de exportacion (#270)

"Limite de exportacion por precio alto" y "Limite de exportacion de la
predescarga" pedian lo mismo que el limite de descarga del sistema, que ya
los acota: un deslizador por funcion solo podia contradecirlo. Ahora ambas
usan la potencia de descarga de la flota y quien quiera exportar menos baja
ese limite (o lo gobierna desde una automatizacion).

La reserva de SOC de la predescarga pasa a 20% por defecto: con 0% el
anticurtailment podia vaciar la flota hasta mi minimo de cada bateria por una
prevision solar que puede no llegar.

Migracion v14 -> v15: borra las dos claves, elimina sus entidades del
registro y suelta la reserva 0% de las instalaciones que nunca activaron la
predescarga, para que alcancen el nuevo defecto.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(pricing): las funciones de precio solo se registran con precios dinamicos

Descarga por precio alto, retencion de excedente por precio y reserva de
descarga abortan si el modo predictivo no es Precios Dinamicos, pero sus
claves se rellenan en toda entrada, asi que la presencia no dice nada del
modo activo: en Franjas Horarias y Precio en Tiempo Real aparecian como
interruptores, deslizadores y filas de estado muertos en el panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(panel): el conmutador de Ajustes avanzados pasa a la cabecera de cada tarjeta

Era un boton global de la barra de organizar que reconstruia toda la pestana
y escondia la tarjeta entera cuando todas sus filas eran avanzadas. Ahora
cada tarjeta con filas `adv` lleva el suyo, con estado propio: las filas se
quedan en el DOM y una clase `adv-off` las oculta por CSS, asi que alternar
no recrea los deslizadores de las demas tarjetas bajo el raton.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: ignora .claude/settings.json

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(panel): separa por grupos la tarjeta de carga predictiva

Cada item de diagPredictive lleva una etiqueta `grp` y el renderizador
dibuja una linea divisoria cuando cambia entre dos filas vivas: base,
precios de carga, predescarga, descarga por precio, estado y accion.
El separador se oculta con la funcion apagada (gatedNodes) y se marca
como avanzado cuando todo el grupo que abre lo es.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(panel): cada estado vuelve al grupo de su funcion

Las cuatro filas de estado estaban juntas al final; ahora cada una cierra
el grupo de su funcion (interruptor + sliders + estado) y el antiguo grupo
"export" se divide en excedente, precio alto y reserva.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(panel): el boton de ajustes avanzados va antes que el de info

El conmutador que cambia lo que muestra la tarjeta encabeza el grupo de
iconos; el margen automatico pasa a el y la separacion se recalcula desde
el nuevo orden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(predictive): el deslizador del margen solar muestra su propio defecto

El controlador caía al 5% de la capacidad de la flota cuando la clave no
estaba en config, pero la entidad number seguía mostrando el "default"
autorizado (0.0). En un alta nueva el panel decía 0.0 kWh mientras el
motor recortaba la previsión con otra cifra: un valor oculto.

- "default" admite un callable y native_value lo resuelve, igual que
  dynamic_bounds hace con min/max.
- Un margen ya guardado (incluido un 0.0 explícito) sigue ganando: el
  defecto solo entra si la clave no está, así que ninguna configuración
  existente se reescribe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changelog): el 5% por defecto se ve en el deslizador

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(migration): la guarda de salida anticipada se quedo en la v14

edaed0d anadio la migracion v14 -> v15 pero dejo el `return` anticipado en
`entry.version >= 14`: una instalacion ya en v14 salia antes de ejecutarla, se
quedaba con las claves de potencia de exportacion y nunca llegaba a v15. Solo
migraban las entradas por debajo de v14.

Los dos tests de flujo HA seguian afirmando la version 14 (de ahi el fallo de
pytest en el PR); el noop de la guarda ahora lee `MarstekVenusConfigFlow.VERSION`
en vez de un numero fijo, que es lo que dejo pasar el despiste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ffunes added a commit that referenced this pull request Sep 22, 2026
…into release/v1.5.0

Los commits locales de #270/#505 llegaron aguas arriba aplastados en 5f0789d,
con commits posteriores del PR encima. El arbol resultante es el de MERGE_HEAD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant