Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a solar battery management #48

Merged
merged 2 commits into from
Jul 18, 2024
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
12 changes: 12 additions & 0 deletions .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ input_number:
unit_of_measurement: A
mode: slider
step: 1
battery_soc:
name: Battery SOC
min: 0
max: 100
icon: mdi:battery
unit_of_measurement: '%'
mode: slider
step: 1

template:
- trigger:
Expand Down Expand Up @@ -193,6 +201,7 @@ solar_optimizer:
action_mode: "service_call"
activation_service: "input_boolean/turn_on"
deactivation_service: "input_boolean/turn_off"
battery_soc_threshold: 30
- name: "Equipement G"
entity_id: "input_boolean.fake_device_g"
power_max: 1200
Expand All @@ -201,6 +210,7 @@ solar_optimizer:
action_mode: "service_call"
activation_service: "input_boolean/turn_on"
deactivation_service: "input_boolean/turn_off"
battery_soc_threshold: 40
- name: "Equipement H"
entity_id: "input_boolean.fake_device_h"
power_entity_id: "input_number.tesla_amps"
Expand All @@ -217,6 +227,7 @@ solar_optimizer:
deactivation_service: "input_boolean/turn_off"
change_power_service: "input_number/set_value"
convert_power_divide_factor: 660
battery_soc_threshold: 50
- name: "Equipement I"
entity_id: "switch.fake_switch_1"
power_max: 20
Expand All @@ -225,6 +236,7 @@ solar_optimizer:
action_mode: "service_call"
activation_service: "switch/turn_on"
deactivation_service: "switch/turn_off"
battery_soc_threshold: 60



Expand Down
12 changes: 11 additions & 1 deletion README-fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@


> ![Nouveau](https://github.com/jmcollin78/solar_optimizer/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> * **release 1.7.0** :
> - ajout d'une gestion d'une batterie. Vous pouvez spécifier une entité de type pourcentage qui donne l'état de charge de la batterie (soc). Sur chaque équipements vous pouvez spécifier un paramètre `battery_soc_threshold` : le seuil de batterie en dessous duquel l'équipement ne sera pas utilisable.
> * **release 1.3.0** :
> - ajout du paramètre `duration_stop_min` qui permet de spécifier une durée minimale de désactivation pour le distinguer du délai minimal d'activation `duration_min`. Si non spécifié, ce paramètre prend la valeur de `duration_min`.
> - restaure l'état des switchs `enable` au démarrage de l'intégration.
Expand Down Expand Up @@ -58,7 +60,9 @@ A chaque équipement configuré est associé une entité de type switch qui perm

Par ailleurs, il est possible de définir une règle d'utilisabilité des équipements. Par exemple, si la voiture est chargée à plus de 90%, l'algorithme considère que l'équipement qui pilote la charge de la voiture doit être éteint. Cette régle est définit sous la forme d'un template configurable qui vaut True si l'équipement est utilisable.

Ces 2 règles permettent à l'algorithme de ne commander que ce qui est réellement utile à un instant t. Ces règles sont ré-évaluées à chaque cycle.
Si une batterie est spécifiée lors du paramétrage de l'intégration et si le seuil `battery_soc_threshold` est spécifié, l'équipement ne sera utilisable que si le soc (pourcentage de charge de la batterie) est supérieur ou égal au seuil.

Ces 3 règles permettent à l'algorithme de ne commander que ce qui est réellement utile à un instant t. Ces règles sont ré-évaluées à chaque cycle.

# Comment on l'installe ?
## HACS installation (recommendé)
Expand Down Expand Up @@ -107,6 +111,7 @@ devices:
action_mode: "service_call"
activation_service: "<service name>
deactivation_service: "switch/turn_off"
battery_soc_threshold: 30
```

Note: les paramètres sous `algorithm` ne doivent pas être touchés sauf si vous savez exactement ce que vous faites.
Expand All @@ -124,6 +129,7 @@ Sous `devices` il faut déclarer tous les équipements qui seront commandés par
| `action_mode` | tous | le mode d'action pour allumer ou éteindre l'équipement. Peut être "service_call" ou "event" (*) | "service_call" | "service_call" indique que l'équipement s'allume et s'éteint via un appel de service. Cf. ci-dessous. "event" indique qu'un évènement est envoyé lorsque l'état doit changer. Cf. (*) |
| `activation_service` | uniquement si action_mode="service_call" | le service a appeler pour activer l'équipement sous la forme "domain/service" | "switch/turn_on" | l'activation déclenchera le service "switch/turn_on" sur l'entité "entity_id" |
| `deactivation_service` | uniquement si action_mode="service_call" | le service a appeler pour désactiver l'équipement sous la forme "domain/service" | "switch/turn_off" | la désactivation déclenchera le service "switch/turn_off" sur l'entité "entity_id" |
| `battery_soc_threshold` | tous | le pourcentage minimal de charge de la batterie pour que l'équipement soit utilisable | 30 | |

Pour les équipements à puissance variable, les attributs suivants doivent être valorisés :

Expand Down Expand Up @@ -155,6 +161,8 @@ devices:
activation_service: "switch/turn_on"
# Le service permettant de désactiver le switch
deactivation_service: "switch/turn_off"
# On autorise le démarrage de la pompe si il y a 10% de batterie dans l'installation solaire
battery_soc_threshold: 10

- name: "Recharge Tesla"
entity_id: "switch.cloucloute_charger"
Expand Down Expand Up @@ -182,6 +190,8 @@ devices:
change_power_service: "number/set_value"
# le facteur permettant de convertir la puissance consigne en Ampères (number.tesla_charging_amps prend des Ampères)
convert_power_divide_factor: 660
# On ne démarre pas une charge si la batterie de l'installation solaire n'est pas chargée à au moins 50%
battery_soc_threshold: 50
...
```
Tout changement dans la configuration nécessite un arrêt / relance de l'intégration (ou de Home Assistant) pour être pris en compte.
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@


>![New](https://github.com/jmcollin78/solar_optimizer/blob/main/images/new-icon.png?raw=true) _*News*_
> * **release 1.7.0**:
> - added battery management. You can specify a percentage type entity that gives the state of charge of the battery (soc). On each device you can specify a `battery_soc_threshold` parameter: the battery threshold below which the device will not be usable.
> * **release 1.3.0**:
> - added the parameter `duration_stop_min` which allows to specify a minimal duration of deactivation to distinguish it from the minimal delay of activation `duration_min`. If not specified, this parameter takes the value of `duration_min`.
> - restores the state of the `enable` switches when the integration starts.
Expand Down Expand Up @@ -58,7 +60,9 @@ Each configured device is associated with a switch-type entity that authorizes t

In addition, it is possible to define a usability rule for equipment. For example, if the car is charged at more than 90%, the algorithm considers that the equipment which controls the charging of the car must be switched off. This rule is defined in the form of a configurable template which is True if the equipment is usable.

These 2 rules allow the algorithm to control only what is really useful at a time t. These rules are re-evaluated at each cycle.
If a battery is specified when configuring the integration and if the threshold `battery_soc_threshold` is specified, the equipment will only be usable if the soc (percentage of battery charge) is greater than or equal to the threshold.

These 3 rules allow the algorithm to only order what is really useful at a given time. These rules are re-evaluated each cycle.

# How do we install it?
## HACS installation (recommended)
Expand Down Expand Up @@ -107,6 +111,7 @@ devices:
action_mode: "service_call"
activation_service: "<service name>"
deactivation_service: "<service name>"
battery_soc_threshold: 30
```

Note: parameters under `algorithm` should not be touched unless you know exactly what you are doing.
Expand All @@ -124,6 +129,7 @@ Under `devices` you must declare all the equipment that will be controlled by So
| `action_mode` | all | the mode of action for turning the equipment on or off. Can be "service_call" or "event" (*) | "service_call" | "service_call" indicates that the equipment is switched on and off via a service call. See below. "event" indicates that an event is sent when the state should change. See (*) |
| `activation_service` | only if action_mode="service_call" | the service to be called to activate the equipment in the form "domain/service" | "switch/turn_on" | activation will trigger the "switch/turn_on" service on the entity "entity_id" |
| `deactivation_service` | only if action_mode="service_call" | the service to call to deactivate the equipment in the form "domain/service" | "switch/turn_off" | deactivation will trigger the "switch/turn_off" service on the entity "entity_id" |
| `battery_soc_threshold` | tous | minimal percentage of charge of the solar battery to enable this device | 30 | |

For variable power equipment, the following attributes must be valued:

Expand Down Expand Up @@ -155,6 +161,8 @@ devices:
activation_service: "switch/turn_on"
# The service to deactivate the switch
deactivation_service: "switch/turn_off"
# We authorize the pump to start if there is 10% battery in the solar installation
battery_soc_threshold: 10

- name: "Tesla Recharge"
entity_id: "switch.cloucloute_charger"
Expand Down Expand Up @@ -182,6 +190,8 @@ devices:
change_power_service: "number/set_value"
# the factor used to convert the set power into Amps (number.tesla_charging_amps takes Amps)
convert_power_divide_factor: 660
# We do not start a charge if the battery of the solar installation is not at least 50% charged
battery_soc_threshold: 50
...
```
Any change in the configuration requires a stop / restart of the integration (or of Home Assistant) to be taken into account.
Expand Down
1 change: 1 addition & 0 deletions custom_components/solar_optimizer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
vol.Optional("convert_power_divide_factor"): vol.Coerce(
float
),
vol.Optional("battery_soc_threshold", default=0): vol.Coerce(float),
}
]
),
Expand Down
3 changes: 3 additions & 0 deletions custom_components/solar_optimizer/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
selector.EntitySelectorConfig(domain=[INPUT_NUMBER_DOMAIN])
),
vol.Optional("smooth_production", default=True): cv.boolean,
vol.Optional("battery_soc_entity_id"): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN])
),
}


Expand Down
9 changes: 7 additions & 2 deletions custom_components/solar_optimizer/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
def get_safe_float(hass, entity_id: str):
"""Get a safe float state value for an entity.
Return None if entity is not available"""
state = hass.states.get(entity_id)
if not state or state.state == "unknown" or state.state == "unavailable":
if entity_id is None or not (state := hass.states.get(entity_id)) or state.state == "unknown" or state.state == "unavailable":
return None
float_val = float(state.state)
return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val
Expand All @@ -38,6 +37,7 @@ class SolarOptimizerCoordinator(DataUpdateCoordinator):
_sell_cost_entity_id: str
_buy_cost_entity_id: str
_sell_tax_percent_entity_id: str
_battery_soc_entity_id: str
_smooth_production: bool
_last_production: float

Expand Down Expand Up @@ -87,6 +87,7 @@ async def configure(self, config: ConfigEntry) -> None:
self._sell_cost_entity_id = config.data.get("sell_cost_entity_id")
self._buy_cost_entity_id = config.data.get("buy_cost_entity_id")
self._sell_tax_percent_entity_id = config.data.get("sell_tax_percent_entity_id")
self._battery_soc_entity_id = config.data.get("battery_soc_entity_id")
self._smooth_production = config.data.get("smooth_production") is True
self._last_production = 0.0

Expand Down Expand Up @@ -143,6 +144,9 @@ async def _async_update_data(self):
self.hass, self._sell_tax_percent_entity_id
)

soc = get_safe_float(self.hass, self._battery_soc_entity_id)
calculated_data["battery_soc"] = soc if soc is not None else 0

#
# Call Algorithm Recuit simulé
#
Expand All @@ -153,6 +157,7 @@ async def _async_update_data(self):
calculated_data["sell_cost"],
calculated_data["buy_cost"],
calculated_data["sell_tax_percent"],
calculated_data["battery_soc"]
)

calculated_data["best_solution"] = best_solution
Expand Down
19 changes: 17 additions & 2 deletions custom_components/solar_optimizer/managed_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ class ManagedDevice:
_deactivation_service: str
_change_power_service: str
_convert_power_divide_factor: int
_battery_soc: float
_battery_soc_threshold: float

def __init__(self, hass: HomeAssistant, device_config):
"""Initialize a manageable device"""
Expand All @@ -133,7 +135,6 @@ def __init__(self, hass: HomeAssistant, device_config):
self._power_max = int(device_config.get("power_max"))
self._power_min = int(device_config.get("power_min") or -1)
self._power_step = int(device_config.get("power_step") or 0)
self._power_step = int(device_config.get("power_step") or 0)
self._can_change_power = self._power_min >= 0
self._convert_power_divide_factor = int(
device_config.get("convert_power_divide_factor") or 1
Expand Down Expand Up @@ -174,6 +175,9 @@ def __init__(self, hass: HomeAssistant, device_config):
self._deactivation_service = device_config.get("deactivation_service")
self._change_power_service = device_config.get("change_power_service")

self._battery_soc = None
self._battery_soc_threshold = float(device_config.get("battery_soc_threshold") or 0)

if self.is_active:
self._requested_power = self._current_power = (
self._power_max if self._can_change_power else self._power_min
Expand Down Expand Up @@ -345,7 +349,7 @@ def is_active(self) -> bool:
@property
def is_usable(self) -> bool:
"""A device is usable for optimisation if the check_usable_template returns true and
if the device is not waiting for the end of its cycle"""
if the device is not waiting for the end of its cycle and if the battery_soc_threshold is >= battery_soc"""

context = {}
now = datetime.now(get_tz(self._hass))
Expand All @@ -356,6 +360,11 @@ def is_usable(self) -> bool:
if not result:
_LOGGER.debug("%s is not usable", self._name)

if result and self._battery_soc is not None and self._battery_soc_threshold is not None:
if self._battery_soc < self._battery_soc_threshold:
result = False
_LOGGER.debug("%s is not usable due to battery soc threshold (%s < %s)", self._name, self._battery_soc, self._battery_soc_threshold)

return result

@property
Expand Down Expand Up @@ -449,6 +458,12 @@ def convert_power_divide_factor(self) -> int:
"""return"""
return self._convert_power_divide_factor

def set_battery_soc(self, battery_soc):
"""Define the battery soc. This is used with is_usable
to determine if the device is usable"""
self._battery_soc = battery_soc


def publish_enable_state_change(self) -> None:
"""Publish an event when the state is changed"""

Expand Down
2 changes: 1 addition & 1 deletion custom_components/solar_optimizer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/jmcollin78/solar_optimizer/issues",
"quality_scale": "silver",
"version": "1.6.0"
"version": "1.7.0"
}
9 changes: 8 additions & 1 deletion custom_components/solar_optimizer/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ async def async_setup_entry(
entity2 = SolarOptimizerSensorEntity(coordinator, hass, "total_power")
entity3 = SolarOptimizerSensorEntity(coordinator, hass, "power_production")
entity4 = SolarOptimizerSensorEntity(coordinator, hass, "power_production_brut")
entity5 = SolarOptimizerSensorEntity(coordinator, hass, "battery_soc")

async_add_entities([entity1, entity2, entity3, entity4], False)
async_add_entities([entity1, entity2, entity3, entity4, entity5], False)

await coordinator.configure(entry)

Expand Down Expand Up @@ -80,13 +81,17 @@ def icon(self) -> str | None:
return "mdi:bullseye-arrow"
elif self.idx == "total_power":
return "mdi:flash"
elif self.idx == "battery_soc":
return "mdi:battery"
else:
return "mdi:solar-power-variant"

@property
def device_class(self) -> SensorDeviceClass | None:
if self.idx == "best_objective":
return SensorDeviceClass.MONETARY
elif self.idx == "battery_soc":
return SensorDeviceClass.BATTERY
else:
return SensorDeviceClass.POWER

Expand All @@ -101,5 +106,7 @@ def state_class(self) -> SensorStateClass | None:
def native_unit_of_measurement(self) -> str | None:
if self.idx == "best_objective":
return "€"
elif self.idx == "battery_soc":
return "%"
else:
return UnitOfPower.WATT
2 changes: 2 additions & 0 deletions custom_components/solar_optimizer/simulated_annealing_algo.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def recuit_simule(
sell_cost: float,
buy_cost: float,
sell_tax_percent: float,
battery_soc: float
):
"""The entrypoint of the algorithm:
You should give:
Expand Down Expand Up @@ -104,6 +105,7 @@ def recuit_simule(
_LOGGER.debug("%s is disabled. Forget it", device.name)
continue

device.set_battery_soc(battery_soc)
usable = device.is_usable
waiting = device.is_waiting
# Force deactivation if active, not usable and not waiting
Expand Down
Loading
Loading