Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
27 changes: 23 additions & 4 deletions README-fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@
- [Configurer les équipements](#configurer-les-équipements)
- [Configurer un équipement simple (on/off)](#configurer-un-équipement-simple-onoff)
- [Configurer un équipement avec une puissance variable](#configurer-un-équipement-avec-une-puissance-variable)
- [Choix de l'algorithme](#choix-de-lalgorithme)
- [Exemples de configurations](#exemples-de-configurations)
- [Commande d'une recharge de Tesla](#commande-dune-recharge-de-tesla)
- [Commande d'une climatisation](#commande-dune-climatisation)
- [Commande du preset d'une climatisation](#commande-du-preset-dune-climatisation)
- [Commande d'un deshumidificateur](#commande-dun-deshumidificateur)
- [Commande pour une lampe](#commande-pour-une-lampe)
- [Commande pour une lampe dimmable](#commande-pour-une-lampe-dimmable)
- [Configurer l'algorithme en mode avancé](#configurer-lalgorithme-en-mode-avancé)
- [Configurer l'algorithme en mode avancé (Recuit Simulé uniquement)](#configurer-lalgorithme-en-mode-avancé-recuit-simulé-uniquement)
- [Entités disponibles](#entités-disponibles)
- [L'appareil "configuration"](#lappareil-configuration)
- [Les appareils](#les-appareils)
Expand Down Expand Up @@ -85,7 +86,10 @@ Le fonctionnement est le suivant :
2. l'algorithme garde la meilleure configuration (celle qui a un cout minimum) et cherche d'autres solutions, jusqu'à ce qu'un minimum soit atteint.
3. la meilleure configuration est alors appliquée.

L'algorithme utilisé est un algorithme de type recuit simulé dont vous trouverez une description ici : https://fr.wikipedia.org/wiki/Recuit_simul%C3%A9
Deux algorithmes d'optimisation sont disponibles, sélectionnables via le champ de configuration `algorithm_type` :

- **Recuit Simulé** (par défaut) : métaheuristique probabiliste qui explore de nombreuses configurations aléatoires et converge vers une solution quasi-optimale. Gère bien les équipements à puissance variable et les compromis complexes. Une description est disponible ici : https://fr.wikipedia.org/wiki/Recuit_simul%C3%A9
- **Greedy Priority (priorité gloutonne)** : algorithme déterministe en deux passes qui allume en priorité les équipements les plus prioritaires (dans la limite du surplus solaire disponible), puis éteint les équipements les moins prioritaires en cas de déficit. Supporte aussi la **délestage** (forcer l'arrêt d'un équipement moins prioritaire en cours de fonctionnement pour libérer de la puissance, lorsque `priority_weight > 0`). Recommandé lorsque l'ordre de priorité des équipements prime sur l'équilibre fin de la puissance.

## Anti-bagot
Pour éviter les effets de bagottements d'un cycle à l'autre, un délai minimal d'activation est paramétrable par équipements : `duration_min`. Par exemple : un chauffe-eau doit être activé au moins une heure pour que l'allumage soit utile, la charge d'une voiture électrique doit durer au moins deux heures, ...
Expand Down Expand Up @@ -166,6 +170,7 @@ Vous devez spécifier :
7. un sensor facultatif qui donne **l'état de charge d'une éventuelle batterie solaire** en pourcentage. Si vous n'avez pas de batterie dans votre installation solaire, laissez ce champ vide,
8. un sensor qui donne la **puissance nette instantanée de charge de la batterie**. Elle doit être exprimée en watt et doit être négative si la batterie charge et positive si la batterie se décharge. Cette valeur sera ajoutée à la puissance net consommée. Si la puissance consommée nette est de -1000 w (vente de 1000 w) mais que la batterie charge de -500 w, cela veut dire que le surplus utilisable par l'algorithme est de 1500 w.
9. **l'heure de début de journée**. A cette heure les compteurs d'uitlisation des équipements sont remis à zéro. La valeur par défaut est 05:00. Pour bien faire, elle doit être avant la première production et le plus tard possible pour les activations en heures creuses.
10. **L'algorithme d'optimisation** (`algorithm_type`). Sélectionne l'algorithme utilisé pour trouver la configuration optimale des équipements. Deux choix : `simulated_annealing` (par défaut) et `greedy_priority`. Voir [Comment fonctionne-t-elle ?](#comment-fonctionne-t-elle-) pour une comparaison.


A part l'état de charge de la batterie solaire, ces informations sont nécessaires à l'algorithme pour fonctionner, elles sont donc toutes obligatoires. Le fait que ce soit des sensor ou input_number permet d'avoir des valeurs qui sont réévaluées à chaque cycle. En conséquence le passage en heure creuse peut modifier le calcul et donc les états des équipements puisque l'import devient moins cher. Donc tout est dynamique et recalculé à chaque cycle.
Expand Down Expand Up @@ -337,8 +342,22 @@ Pour allumer une lampe témoin de production disponible:
offpeak_time: "02:00"
```

## Configurer l'algorithme en mode avancé
La configuration avancée permet de modifier la configuration de l'algorithme. Il n'est pas conseillé d'y toucher mais cette fonction reste disponible pour des besoins spécifiques. L'algorithme est un algorithme de type recuit simulé qui cherche des configurations (combinaisons de on/off) et procède à une évaluation d'une fonction de coût à chaque itération.
## Choix de l'algorithme

Solar Optimizer propose deux algorithmes d'optimisation. Choisissez en fonction de vos besoins :

| | Recuit Simulé (par défaut) | Greedy Priority |
|---|---|---|
| **Stratégie** | Recherche probabiliste sur de nombreuses configurations aléatoires | Déterministe en deux passes : allume les plus prioritaires, éteint les moins prioritaires si déficit |
| **Puissance variable** | Prise en charge complète | Non supporté |
| **Délestage** | Non supporté | Supporté (`priority_weight > 0`) |
| **Usage CPU** | Plus élevé (~1000 itérations par cycle) | Plus faible (O(n log n) par cycle) |
| **Idéal pour** | Équipements à puissance variable (chargeur VE, routeur solaire) | Équipements fixes où l'ordre de priorité prime |

Définissez `algorithm_type` dans la configuration de l'intégration à `simulated_annealing` (par défaut) ou `greedy_priority`.

## Configurer l'algorithme en mode avancé (Recuit Simulé uniquement)
La configuration avancée permet de modifier la configuration de l'algorithme de **Recuit Simulé**. Ces paramètres n'ont aucun effet lorsque `algorithm_type: greedy_priority` est sélectionné. Il n'est pas conseillé d'y toucher mais cette fonction reste disponible pour des besoins spécifiques. L'algorithme est un algorithme de type recuit simulé qui cherche des configurations (combinaisons de on/off) et procède à une évaluation d'une fonction de coût à chaque itération.
A chaque itération, l'algorithme échange de façon aléatoire l'état de certains équipements et évalue la fonction de cout. Si l'évaluation est meilleure que la prédédente elle est gardée. Si elle est plus forte, elle peut être gardée en fonction d'une "température". Cette température va baisser au fur et à mesure des itérations ce qui va permettre de converger vers une solution quasi optimale.

Pour utiliser la configuration avancée, vous devez :
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [Configure the Devices](#configure-the-devices)
- [Configuring a Simple Device (On/Off)](#configuring-a-simple-device-onoff)
- [Configuring a Device with Variable Power](#configuring-a-device-with-variable-power)
- [Algorithm Selection](#algorithm-selection)
- [Configuration Examples](#configuration-examples)
- [Controlling Tesla Charging](#controlling-tesla-charging)
- [Controlling an Air Conditioner hvac mode](#controlling-an-air-conditioner-hvac-mode)
Expand Down Expand Up @@ -86,7 +87,10 @@ The operation is as follows:
2. the algorithm keeps the best configuration (the one with a minimum cost) and looks for other solutions, until a minimum is reached.
3. the best configuration is then applied.

The algorithm used is a simulated annealing type algorithm, a description of which you will find here: https://fr.wikipedia.org/wiki/Recuit_simul%C3%A9
Two optimization algorithms are available, selectable via the `algorithm_type` configuration field:

- **Simulated Annealing** (default): a probabilistic metaheuristic that explores many random configurations and converges on a near-optimal solution. It handles variable-power devices and complex trade-offs well. A description can be found here: https://fr.wikipedia.org/wiki/Recuit_simul%C3%A9
- **Greedy Priority**: a fast deterministic two-pass algorithm that turns on the highest-priority devices first (within available solar surplus), then turns off the lowest-priority devices if there is a deficit. It also supports **load shedding** — forcing a lower-priority active device off to free power for a higher-priority waiting device (when `priority_weight > 0`). Recommended when device priority ordering matters more than fine-grained power balance.

## Anti-flickering
To avoid the effects of flickering from one cycle to another, a minimum activation delay can be configured by equipment: `duration_min`. For example: a water heater must be activated for at least one hour for the ignition to be useful, charging an electric car must last at least two hours, ...
Expand Down Expand Up @@ -178,6 +182,7 @@ You need to specify:
7. An optional sensor that provides **the charge level of a possible solar battery** in percentage. If your solar installation does not include a battery, leave this field empty.
8. A sensor that provides **the net instantaneous charging power of the battery**. It must be expressed in watt and should be negative when the battery is charging and positive when the battery is discharging. This value will be added to the net consumed power. If the net consumed power is -1000 W (selling 1000 W) but the battery is charging at -500 W, it means that the surplus available for the algorithm is 1500 W.
9. **The start time of the day**. At this time, the usage counters of the equipment are reset to zero. The default value is 05:00. Ideally, this should be set before the first production of the day and as late as possible for off-peak activations.
10. **The optimization algorithm** (`algorithm_type`). Selects the algorithm used to find the optimal device configuration. Two choices: `simulated_annealing` (default) and `greedy_priority`. See [How does it work?](#how-does-it-work) for a comparison.

Except for the solar battery charge level, these parameters are essential for the algorithm to function, so they are all mandatory. Using sensors or `input_number` allows values to be updated in real-time at each cycle. Consequently, when off-peak hours begin, the calculation may change, impacting the state of the equipment as importing energy becomes cheaper. Everything is dynamic and recalculated in each cycle.

Expand Down Expand Up @@ -352,8 +357,22 @@ To control light brightness
offpeak_time: "02:00"
```

## Configuring the Algorithm in Advanced Mode
Advanced configuration allows modifying the algorithm's settings. It is not recommended to change these settings unless you have specific needs. The algorithm uses a **simulated annealing** approach to search for optimal configurations (combinations of on/off states) and evaluates a cost function at each iteration.
## Algorithm Selection

Solar Optimizer supports two optimization algorithms. Choose based on your needs:

| | Simulated Annealing (default) | Greedy Priority |
|---|---|---|
| **Strategy** | Probabilistic search over many random configurations | Deterministic two-pass: turn on high-priority first, turn off low-priority if deficit |
| **Variable power** | Full support | Not supported |
| **Load shedding** | Not supported | Supported (`priority_weight > 0`) |
| **CPU usage** | Higher (~1000 iterations per cycle) | Lower (O(n log n) per cycle) |
| **Best for** | Variable-power devices (EV charger, solar router) | Fixed-power devices where priority ordering matters |

Set `algorithm_type` in the integration configuration to `simulated_annealing` (default) or `greedy_priority`.

## Configuring the Algorithm in Advanced Mode (Simulated Annealing only)
Advanced configuration allows modifying the **Simulated Annealing** algorithm's settings. These settings have no effect when `algorithm_type: greedy_priority` is selected. It is not recommended to change these settings unless you have specific needs. The algorithm uses a **simulated annealing** approach to search for optimal configurations (combinations of on/off states) and evaluates a cost function at each iteration.

During each iteration, the algorithm randomly changes the state of some devices and evaluates the cost function. If the new evaluation is better than the previous one, it is kept. If it is worse, it may still be kept based on a "temperature" parameter. This temperature gradually decreases over iterations, allowing the algorithm to converge toward an optimal solution.

Expand Down
43 changes: 43 additions & 0 deletions custom_components/solar_optimizer/algorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Protocol definition for Solar Optimizer algorithms."""

from typing import Protocol, runtime_checkable

from .managed_device import ManagedDevice


@runtime_checkable
class SolarOptimizerAlgorithm(Protocol):
"""Protocol that all optimization algorithms must implement."""

def recuit_simule(
self,
devices: list[ManagedDevice],
power_consumption: float,
solar_power_production: float,
allowed_power_overage: float,
sell_cost: float,
buy_cost: float,
sell_tax_percent: float,
battery_soc: float,
priority_weight: int,
) -> tuple[list[dict], float, float]:
"""Run one optimization cycle.

Args:
devices: list of managed devices to optimize
power_consumption: current net grid consumption in W (including battery charge)
solar_power_production: current solar production in W
allowed_power_overage: max W of grid import allowed to turn on a device (Greedy only)
sell_cost: grid sell price in €/kWh
buy_cost: grid buy price in €/kWh
sell_tax_percent: tax applied to sold energy (%)
battery_soc: battery state of charge 0-100
priority_weight: how much device priority influences decisions (0-100)

Returns:
(solution, objective, total_power):
solution: list of device dicts with at minimum 'name', 'state', 'requested_power'
objective: scalar cost metric (lower = better)
total_power: total W consumed by active devices in solution
"""
...
9 changes: 9 additions & 0 deletions custom_components/solar_optimizer/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN])
),
vol.Optional(CONF_RAZ_TIME, default=DEFAULT_RAZ_TIME): str,
vol.Optional(CONF_ALLOWED_POWER_OVERAGE_PERCENT, default=0): selector.NumberSelector(
Comment thread
jmcollin78 marked this conversation as resolved.
selector.NumberSelectorConfig(min=0, max=100, step=1, mode=selector.NumberSelectorMode.SLIDER, unit_of_measurement="%")
),
vol.Optional(CONF_ALGORITHM_TYPE, default=ALGORITHM_SIMULATED_ANNEALING): selector.SelectSelector(
selector.SelectSelectorConfig(
options=ALGORITHM_TYPES,
mode=selector.SelectSelectorMode.LIST,
)
),
}
)

Expand Down
6 changes: 6 additions & 0 deletions custom_components/solar_optimizer/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@
CONF_MAX_ON_TIME_PER_DAY_MIN = "max_on_time_per_day_min"
CONF_MIN_ON_TIME_PER_DAY_MIN = "min_on_time_per_day_min"
CONF_OFFPEAK_TIME = "offpeak_time"
CONF_ALLOWED_POWER_OVERAGE_PERCENT = "allowed_power_overage_percent"

CONF_ALGORITHM_TYPE = "algorithm_type"
ALGORITHM_SIMULATED_ANNEALING = "simulated_annealing"
ALGORITHM_GREEDY_PRIORITY = "greedy_priority"
ALGORITHM_TYPES = [ALGORITHM_SIMULATED_ANNEALING, ALGORITHM_GREEDY_PRIORITY]

PRIORITY_WEIGHT_NULL = "None"
PRIORITY_WEIGHT_LOW = "Low"
Expand Down
27 changes: 26 additions & 1 deletion custom_components/solar_optimizer/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,18 @@

from homeassistant.config_entries import ConfigEntry

from .const import DEFAULT_REFRESH_PERIOD_SEC, name_to_unique_id, SOLAR_OPTIMIZER_DOMAIN, DEFAULT_RAZ_TIME
from .const import (
DEFAULT_REFRESH_PERIOD_SEC,
name_to_unique_id,
SOLAR_OPTIMIZER_DOMAIN,
DEFAULT_RAZ_TIME,
CONF_ALGORITHM_TYPE,
ALGORITHM_GREEDY_PRIORITY,
CONF_ALLOWED_POWER_OVERAGE_PERCENT,
)
from .managed_device import ManagedDevice
from .simulated_annealing_algo import SimulatedAnnealingAlgorithm
from .greedy_priority_algo import GreedyPriorityAlgorithm

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -68,6 +77,7 @@ def __init__(self, hass: HomeAssistant, config):
self._battery_soc_entity_id: str = None
self._battery_charge_power_entity_id: str = None
self._raz_time: time = None
self._allowed_power_overage_percent: float = 0.0

self._central_config_done = False
self._priority_weight_entity = None
Expand Down Expand Up @@ -127,6 +137,17 @@ async def configure(self, config: ConfigEntry) -> None:
self._raz_time = datetime.strptime(
config.data.get("raz_time") or DEFAULT_RAZ_TIME, "%H:%M"
).time()

self._allowed_power_overage_percent = float(
config.data.get(CONF_ALLOWED_POWER_OVERAGE_PERCENT) or 0
)

if config.data.get(CONF_ALGORITHM_TYPE) == ALGORITHM_GREEDY_PRIORITY:
self._algo = GreedyPriorityAlgorithm()
_LOGGER.info("Solar Optimizer using Greedy Priority algorithm")
else:
_LOGGER.info("Solar Optimizer using Simulated Annealing algorithm")

self._central_config_done = True

async def on_ha_started(self, _) -> None:
Expand Down Expand Up @@ -194,10 +215,14 @@ async def _async_update_data(self):
#
# Call Algorithm Recuit simulé
#
allowed_power_overage = (
calculated_data["power_production"] * self._allowed_power_overage_percent / 100.0
)
best_solution, best_objective, total_power = self._algo.recuit_simule(
self._devices,
calculated_data["power_consumption"] + calculated_data["battery_charge_power"],
calculated_data["power_production"],
allowed_power_overage,
calculated_data["sell_cost"],
calculated_data["buy_cost"],
calculated_data["sell_tax_percent"],
Expand Down
Loading