Bonjour,
ATTENTION : Ce sujet est désormais obsolète et un nouveau sujet est disponible ici
J’ai trouvé ici beaucoup de ressources pour m’aider à basculer de mon installation Jeedom à mon installation HA. Merci à tous.
Il est désormais temps de partager un peu à mon tour.
Ce tutoriel est pour gérer sa piscine analysé par une sonde EcO iopool.
Sur Jeedom, j’ai développé un plugin et je voulais retrouver le même comportement à savoir :
- Connaitre l’état de l’eau remonté par la sonde EcO dans HA
- Gérer la durée de filtration de l’eau à partir de la recommandation de iopool (basé sur 2 slots dans la journée)
Etape 1 : Les sensors
iopool met à disposition une API publique disponible ici : API publique iopool | iopool FAQ
Elle nécessite une clé API que vous pouvez trouver directement dans l’application iopool en allant dans vos paramètres (Plus > Paramètres)
Il vous faudra aussi votre id de bassin que vous pouvez trouver avec cette commande (nécessite d’avoir CURL installé ) :
curl --header 'x-api-key: icimacléapiiopool' https://api.iopool.com/v1/pools/
Le premier champs (id) du retour de la commande, sera votre id de bassin. Si vous avez plusieurs sondes active, choisissez le bon bassin.
Les sensors indiqués ci-dessous sans dans un package. Pour l’utiliser, il faut dans le fichier configuration.yaml, ajouter ceci :
homeassistant:
# Load packages
packages: !include_dir_merge_named includes/packages
Nous allons aussi déclaré notre apikey dans le fichier secrets.yaml
# iopool
iopool_api_key: icimacléapiiopool
Vous pouvez ne pas l’utiliser et mettre directement votre clé API en lieu et place du !secret iopool_api_key
ci-dessous.
Il vous faudra aussi modifier le contenu ci-dessous pour inclure l’id de bassin que vous venez de récupérer
Le contenu ci-dessous sera donc à mettre dans le repertoire includes/packages (à moins de le changer ci-dessus), dans un fichier de votre choix. Pour ma part, je l’ai nommé pool.yaml.
pool:
sensor:
- platform: rest
unique_id: fabc1ee2-0bbe-416e-b23d-2474ac25fe4e
name: iopool
resource: https://api.iopool.com/v1/pool/<votre_id_de_bassin_ici>
value_template: "{{ value_json.title }}"
json_attributes:
- id
- latestMeasure
- hasAnActionRequired
- advice
- mode
headers:
x-api-key: !secret iopool_api_key
scan_interval: 300
icon: mdi:pool
- platform: history_stats
name: Temps de filtration écoulé
entity_id: switch.pool_switch
state: "on"
type: time
start: "{{ now().replace(hour=0, minute=0, second=0) }}"
end: "{{ now() }}"
template:
- sensor:
- name: "Température Sonde Piscine"
unique_id: b336b008-dc88-4e3b-afd9-d662979fb0c1$
state: "{{ state_attr('sensor.iopool', 'latestMeasure')['temperature'] | round(2) }}"
device_class: temperature
unit_of_measurement: "°C"
state_class: measurement
icon: mdi:pool-thermometer
attributes:
source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
- name: "pH Sonde Piscine"
unique_id: f4804a67-1224-4507-a4fb-21d983958b7c
state: "{{ state_attr('sensor.iopool', 'latestMeasure')['ph'] | round(1) }}"
unit_of_measurement: "pH"
attributes:
source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
- name: "Capacité de désinfection Sonde Piscine"
unique_id: e0ef9122-c53a-41ae-be72-517f3fcbb443
state: "{{ state_attr('sensor.iopool', 'latestMeasure')['orp'] | round(0) }}"
unit_of_measurement: "mV"
attributes:
source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
- name: "Recommandation Durée de Filtration Sonde Piscine"
unique_id: f53659ba-922f-4861-9198-73a7dd43ae6a
state: "{{ state_attr('sensor.iopool', 'advice')['filtrationDuration'] * 60 }}"
device_class: duration
unit_of_measurement: "min"
icon: mdi:sun-clock-outline
- name: "Mode Sonde Piscine"
unique_id: af6db587-be33-44e7-950c-fa52f0453d1f
state: "{{ state_attr('sensor.iopool', 'mode') }}"
icon: mdi:auto-mode
- name: "Heure fin de Filtration Piscine Slot1"
unique_id: bb04f92f-29a3-47b2-ad4f-81af2a850f02
state: "{{ states('input_datetime.pool_stop_slot1') }}"
icon: mdi:clock-outline
- name: "Heure fin de Filtration Piscine Slot2"
unique_id: cde90d21-b10a-4259-9767-bc2d174a7ca6
state: "{{ states('input_datetime.pool_stop_slot2') }}"
icon: mdi:clock-outline
- binary_sensor:
- name: "Actions requises Sonde Piscine"
unique_id: fb6bb7e0-86ad-4f27-90ee-47c39db0ab12
state: "{{ state_attr('sensor.iopool', 'hasAnActionRequired') }}"
device_class: problem
icon: mdi:checkbox-marked-circle-plus-outline
input_datetime:
pool_start_slot1:
name: Heure début de Filtration Piscine Slot1
icon: mdi:clock-outline
has_date: false
has_time: true
pool_stop_slot1:
name: Heure fin de Filtration Piscine Slot1
icon: mdi:clock-outline
has_date: false
has_time: true
pool_start_slot2:
name: Heure début de Filtration Piscine Slot2
icon: mdi:clock-outline
has_date: false
has_time: true
pool_stop_slot2:
name: Heure fin de Filtration Piscine Slot2
icon: mdi:clock-outline
has_date: false
has_time: true
input_select:
pool_boost:
name: Boost de Filtration Piscine
icon: mdi:plus-box-multiple
options:
- Aucun
- 1H
- 4H
- 8H
- 24H
pool_mode:
name: Mode
icon: mdi:sun-snowflake-variant
options:
- Standard
- Active
- Passive
timer:
pool_boost:
name: Timer de boost
restore: true
icon: mdi:timer-plus
input_number:
filtration_duration_calculated:
name: Temps de filtration calculé
icon: mdi:clock-outline
step: 1
min: 0
max: 1440
Détaillons un peu le contenu :
-
Le sensor en plateform rest, permet de récupérer toutes les 5 minutes, les informations par l’API iopool en les stockant dans les attributs du sensor. La valeur du sensor est le nom de votre sonde.
-
Un second sensor est créé pour connaitre le temps de filtration déjà effectuée sur la journée. Il vous faudra remplacer le entity_id switch.pool_switch par le switch correspond à la commande de votre pompe
-
Ensuite, dans la partie template, nous créons un sensors pour chaque informations utiles récupérée par l’appel API précédent (temperature, pH, ORP, recommandation de filtration, mode de la piscine, actions requises) et nous ajoutons 2 sensors supplémentaires permettant de définir l’heure de fin des 2 plages de filtration prévue (cf. le chapitre filtration)
-
Nous créons ensuite 4 input_datetime de type heure uniquement pour l’heure de début et fin de chaque plage de filtration (cf. le chapitre filtration)
-
Les input select permette de gérer une liste de durée de boost bien utile lors de l’application des produits ((cf. le chapitre filtration) ainsi que le mode de la piscine (standard = Eté / Active = Hivernage Actif / Passive = Hivernage Passif).
-
Un timer qui permet de suivre le temps restant d’un boost
-
Un input_number qui permet de stocker le temps de filtration calculé (car vous verrez dans le chapitre filtration, l’on peut surcharger le temps de filtration recommandé d’iopool).
Ne pas oublier de vérifier et relancer votre configuration
.
Avant de passer à l’étape suivante, vérifiez que vos entités ont bien récupéré les informations par API à commencer par le sensor.iopool
puisque c’est de lui que les autres entités vont avoir les informations.
Etape 2 : La filtration
Pour gérer l’intelligence de la filtration, j’ai décidé d’utiliser AppDaemon qui est un addon. Vous trouverez facilement des tuto sur l’installation.
Point particulier, dans la configuration de appDaemon il faut ajouter un package python humanize
.
Pour cela, il suffit d’aller dans la configuration de AppDaemon et dans le champs Python Package
saisir humanize
et faire ‹ entrer › afin qu’il apparaisse ainsi
et redémarrer l’addon AppDaemon
Une fois installé et configuré, il faut mettre en place l’app. Pour cela, dans /config/appdaemon/apps, créer un fichier pool_pump_manager.py
avec le contenu suivant :
import hassapi as hass
import math
from datetime import datetime, timedelta, time
import humanize
"""
Describe here
"""
class pool_pump_manager(hass.Hass):
# Initialize Class
# ----------------
def initialize(self):
self.log("Initialized")
# Verify app configuration conformity
if (self._check_configuration() != True):
self.log("Error in configuration. Stopping app waiting configuration correction")
self.stop_app(self.name)
raise RuntimeError("Invalid configuration. Please look critical events in logs")
self.log("INIT - Pool is in mode %s", self.get_state(self.args['mode_sensor_id']))
# Depending of mode in progress
if self.get_state(self.args['mode_sensor_id']).lower() == "Standard".lower():
# Create task for slots
self.slot1_start_handler = self._generate_tasks(1, self.get_state(self.args['times'][0]['start_time_id']))
self.slot2_start_handler = self._generate_tasks(2, self.get_state(self.args['times'][1]['start_time_id']))
# Create end task SLOT1 if not exist - for security in case app is reload
self.slot1_end_handler = ""
if self.timer_running(self.slot1_end_handler) == False:
self.slot1_end_handler = self.run_at(self.callback_stop_filtration, self.get_state(self.args['times'][0]['stop_time_id']), slot=1, message="Slot1 Automation")
self.log("INIT - Based on slot1 end time in %s, we create a task with handler %s at %s", self.args['times'][0]['stop_time_id'], self.slot1_end_handler, self.get_state(self.args['times'][0]['stop_time_id']), level = "INFO")
# Create end task SLOT2 if not exist - for security in case app is reload
self.slot2_end_handler = ""
if self.timer_running(self.slot2_end_handler) == False:
self.slot2_end_handler = self.run_at(self.callback_stop_filtration, self.get_state(self.args['times'][1]['stop_time_id']), slot=2, message="Slot2 Automation")
self.log("INIT - Based on slot2 end time in %s, we create a task with handler %s at %s", self.args['times'][1]['stop_time_id'], self.slot2_end_handler, self.get_state(self.args['times'][1]['stop_time_id']), level = "INFO")
# Listen change on slot times
self.listen_state(self.callback_change_start_slots, self.args['times'][0]['start_time_id'], duration=1, slot=1)
self.listen_state(self.callback_change_start_slots, self.args['times'][1]['start_time_id'], duration=1, slot=2)
elif self.get_state(self.args['mode_sensor_id']).lower() == "Active".lower():
# Create the start task for active winter filtration
self.run_daily(self.callback_start_filtration, self.args['winter'][0]['start_time'], message="Active Winter")
# Create the end task for active winter filtration
self.run_daily(self.callback_stop_filtration, (datetime.strptime(self.args['winter'][0]['start_time'], "%H:%M:%S") + timedelta(minutes=self.args['winter'][0]['duration_minutes'])).time(), message="Active Winter")
# Listen change in mode sensor
self.listen_state(self.callback_change_mode, self.args['mode_sensor_id'], duration=1)
# Listen change in boost selector and timer
self.listen_state(self.callback_boost_trigger, self.args['boost_selector_sensor_id'])
self.listen_event(self.callback_boost_timer_finished, entity_id = self.args['boost_timer_sensor_id'], event = "timer.finished")
# List all existing handler for this app
self._list_all_handler()
# Check if all app configuration is good. In case of error, we stopping app
# -------------------------------------------------------------------------
def _check_configuration(self) -> bool:
if self.entity_exists(self.args['pump_sensor_id']) == False:
self.log("Entity defined in 'pump_sensor_id' configuration don't exist. Please update your configuration !", level = "CRITICAL")
return False
if self.entity_exists(self.args['pool_filtration_duration_sensor_id']) == False:
self.log("Entity defined in 'pool_filtration_duration_sensor_id' configuration don't exist. Please update your configuration !", level = "CRITICAL")
return False
if self.entity_exists(self.args['mode_sensor_id']) == False:
self.log("Entity defined in 'mode_sensor_id' configuration don't exist. Please update your configuration !", level = "CRITICAL")
return False
if self.entity_exists(self.args['times'][0]['start_time_id']) == False:
self.log("Entity defined in 'times.slot1.start_time_id' configuration don't exist. Please update your configuration !", level = "CRITICAL")
return False
if self.entity_exists(self.args['times'][1]['start_time_id']) == False:
self.log("Entity defined in 'times.slot2.start_time_id' configuration don't exist. Please update your configuration !", level = "CRITICAL")
return False
if not math.isclose(self.args['times'][0]['duration_percent'] + self.args['times'][1]['duration_percent'], 1, rel_tol=0, abs_tol=0):
self.log("Sum of all duration_percent in times configuration is not equal to 1. Please update your configuration !", level = "CRITICAL")
return False
if datetime.strptime(self.get_state(self.args['times'][0]['start_time_id']), "%H:%M:%S") >= datetime.strptime(self.get_state(self.args['times'][1]['start_time_id']), "%H:%M:%S"):
self.log("Slot1 start time is before or equal to slot2 start time. Please update your configuration !", level = "CRITICAL")
return False
if self.entity_exists(self.args['pool_filtration_duration_storage']) == False:
self.log("Entity defined in 'pool_filtration_duration_storage' configuration don't exist. Please update your configuration !", level = "CRITICAL")
return False
if 'pool_filtration_duration_min' in self.args and 'pool_filtration_duration_max' in self.args and self.args['pool_filtration_duration_min'] > self.args['pool_filtration_duration_max']:
self.log("pool_filtration_duration_min (%i) cannot be higher than pool_filtration_duration_max (%i)", self.args['pool_filtration_duration_min'], self.args['pool_filtration_duration_max'], level = "CRITICAL")
return False
return True
# Return Filtration Duration
# --------------------------
def _get_filtration_duration(self) -> int:
filtration_duration = int(float(self.get_state(self.args['pool_filtration_duration_sensor_id'])))
if 'pool_filtration_duration_min' in self.args and filtration_duration < self.args['pool_filtration_duration_min']:
filtration_duration = self.args['pool_filtration_duration_min']
if 'pool_filtration_duration_max' in self.args and filtration_duration > self.args['pool_filtration_duration_max']:
filtration_duration = self.args['pool_filtration_duration_max']
self.set_value(self.args['pool_filtration_duration_storage'], filtration_duration)
self.log("Based on configuration, filtration duration is %i minutes (iopool recommandation is %i minutes)", filtration_duration, int(float(self.get_state(self.args['pool_filtration_duration_sensor_id']))), level = "INFO")
return filtration_duration
# Callback to restart app and force a new initialize when mode change
# If mode is Standard or Active
# With this, we trigger new tasks by cleaning all old tasks
# -------------------------------------------------------------------
def callback_change_mode(self, entity, attribute, old, new, **kwargs: dict) -> None:
self.log("New mode detected : %s (Old: %s)", new, old, level = "INFO")
self.restart_app(self.name)
# Generate scheduled task
# -----------------------
def _generate_tasks(self, slot: int, time: str) -> dict:
if slot == 1:
slot_start_handler = self.run_daily(self.callback_slot_start, time, slot=slot)
elif slot == 2:
slot_start_handler = self.run_daily(self.callback_slot_start, time, slot=slot)
self.log("Generate a daily task at %s for slot%i start", time, slot, level = "INFO")
return slot_start_handler
# Callback when one slot start
# ----------------------------
def callback_slot_start(self, **kwargs: dict) -> None:
if kwargs['slot'] == 1:
# Start filtration
self.callback_start_filtration(message="Pump starting for slot {}".format(kwargs['slot']))
# Calculate time delta between slot1 and slot2
timedelta_minutes = int((datetime.strptime(self.get_state(self.args['times'][1]['start_time_id']), "%H:%M:%S") - datetime.strptime(self.get_state(self.args['times'][0]['start_time_id']), "%H:%M:%S")).total_seconds() / 60)
# Calculate filtration duration based on configuration percent and iopool filtration time recommandation
filtration_duration = self._get_filtration_duration() * float(self.args['times'][0]['duration_percent'])
self.log("Duration Between slots : %s minutes / Filtration Duration : %s (minutes)", int(timedelta_minutes), int(filtration_duration), level = "DEBUG")
# If time between slot1 and slot 2 is superior than filtration time
if int(timedelta_minutes) > int(filtration_duration):
start_time = datetime.strptime(self.get_state(self.args['times'][0]['start_time_id']), "%H:%M:%S")
end_time = datetime.strftime(start_time + timedelta(minutes=filtration_duration), "%H:%M:%S")
# Setting end time in a HA input_datetime for slot1
self.set_state(self.args['times'][0]['stop_time_id'], state=end_time)
self.log("Slot1 Filtration times - Start : %s / End : %s", start_time.time(), end_time, level = "INFO")
# If an existing timer run for self.slot1_end_handler handler, we cancel it
if self.timer_running(self.slot1_end_handler) == True:
self.cancel_timer(self.slot1_end_handler)
self.log("Cancel existing timer for slot1 with handler %s", self.slot1_end_handler, level = "INFO")
# We create an new timer based on stored input_datetime - end of slot1
self.slot1_end_handler = self.run_at(self.callback_stop_filtration, end_time, slot=1, message="Slot1 Automation")
self.log("Create new timer for slot1 with handler %s", self.slot1_end_handler, level = "INFO")
else:
self.log("Duration between slot1 and slot2 start time (%s minutes) is less than time need for filtration (%s minutes). We will not stop filtration for slot1", timedelta_minutes, filtration_duration, level="WARNING")
elif kwargs['slot'] == 2:
# Retrieves the time (in minutes) the pump was already running today
elapsed_filtration_minutes = int(float(self.get_state(self.args['pump_stats_sensor_id'])) * 60)
# Calculate the remaining time of filtration
remaining_filtration_minutes = self._get_filtration_duration() - elapsed_filtration_minutes
self.log("Slot2 - Elapsed time of filtration : %s minutes / Remaining time : %s minutes", elapsed_filtration_minutes, remaining_filtration_minutes, level = "INFO")
start_time = datetime.strptime(self.get_state(self.args['times'][1]['start_time_id']), "%H:%M:%S")
end_time = datetime.strftime(start_time + timedelta(minutes=remaining_filtration_minutes), "%H:%M:%S")
# Setting end time in a HA input_datetime for slot2
self.set_state(self.args['times'][1]['stop_time_id'], state=end_time)
self.log("Slot2 Filtration times - Start : %s / End : %s", start_time.time(), end_time, level = "INFO")
# If an existing timer run for self.slot2_end_handler handler, we cancel it
if self.timer_running(self.slot2_end_handler) == True:
self.cancel_timer(self.slot2_end_handler)
self.log("Cancel existing timer for slot2 with handler %s", self.slot2_end_handler, level = "INFO")
# If remaining time is superior than 0
if remaining_filtration_minutes > 0:
# Start filtration
self.callback_start_filtration(message="Pump starting for slot {}".format(kwargs['slot']))
# We create an new timer based on stored input_datetime - end of slot1
self.slot2_end_handler = self.run_at(self.callback_stop_filtration, end_time, slot=2, message="Slot2 Automation")
self.log("Create new timer for slot2 with handler %s", self.slot2_end_handler, level = "INFO")
else:
self.log("There is no more filtration time left to do", level = "INFO")
# Callback when start time change on slot1 or slot2
# ---------------------------------------------
def callback_change_start_slots(self, entity, attribute, old, new, **kwargs: dict) -> None:
self.log("Change detected in %s time - old value %s / new value : %s", entity, old, new, level = "INFO")
# Generate new task for a specific slot
slot_handler = self._generate_tasks(kwargs['slot'], self.get_state(entity))
self.log("Generate a new task with handler %s for slot%i - at %s", slot_handler, kwargs['slot'], new, level = "INFO")
if kwargs['slot'] == 1:
# Cancel previous handler
self.cancel_timer(self.slot1_start_handler)
# Store new handler
self.slot1_start_handler = slot_handler
elif kwargs['slot'] == 2:
# Cancel previous handler
self.cancel_timer(self.slot2_start_handler)
# Store new handler
self.slot2_start_handler = slot_handler
self.log("New handler %s stored for slot%i", slot_handler, kwargs['slot'], level = "DEBUG")
# Callback who start filtration
# -----------------------------
def callback_start_filtration(self, **kwargs: dict) -> None:
if self.get_state(self.args['pump_sensor_id']).lower() == 'off':
self.log("Starting Pump - %s", kwargs['message'], level = "INFO")
self.turn_on(self.args['pump_sensor_id'])
else:
self.log("%s - Pump is already On !", kwargs['message'], level = "INFO")
# Callback who stop filtration
# ----------------------------
def callback_stop_filtration(self, **kwargs: dict) -> None:
if self._boost_in_progress() == False:
if self.get_state(self.args['pump_sensor_id']).lower() == 'on':
self.log("Stopping Pump - %s", kwargs['message'], level = "INFO")
self.turn_off(self.args['pump_sensor_id'])
else:
self.log("Pump is already Off !", level = "INFO")
else:
self.log("Boost is in progress (%s). Pump stopping is canceled.", self.get_state(self.args['boost_selector_sensor_id']), level = "INFO")
# If slot is in method arguments and equal to slot2, we send a notification
if 'slot' in kwargs and kwargs['slot'] == 2:
self._end_notification()
# Callback triggerred when boost time change is detected
# ------------------------------------------------------
def callback_boost_trigger(self, entity, attribute, old, new, **kwargs: dict) -> None:
self.log("Change in boost trigger detected : %s (Old: %s)", new, old, level = "INFO")
if new != "Aucun":
boost_duration_hour = int(''.join(filter(str.isdigit, new)))
boost_duration_timer = (datetime(1900, 1, 1) + timedelta(hours=boost_duration_hour)).strftime('%H:%M:%S')
self.call_service("timer/start", entity_id=self.args['boost_timer_sensor_id'], duration=boost_duration_timer)
self.callback_start_filtration(message="Boost {}".format(new))
elif new == "Aucun":
self.call_service("timer/cancel", entity_id=self.args['boost_timer_sensor_id'])
if self._is_during_slot() is False:
self.callback_stop_filtration(message="Stopping boost {}".format(old))
self.log("Boost mode change from %s to %s. Stopping filtration", old, new, level = "INFO")
else:
self.log("As boost change from mode %s to %s during normal filtration time, pump stopping is canceled", old, new, level = "INFO")
# Triggered when boost time finish to stop pump if we are not in normal filtration periods
# ----------------------------------------------------------------------------------------
def callback_boost_timer_finished(self, event, data, **kwargs: dict) -> None:
self.log("Boost Timer finish (%s) with data: %s", event, data, level = "INFO")
if self._is_during_slot() is False:
self.log("Boost timer finished at %s - Stopping filtration", data['finished_at'], level = "INFO")
self._send_notification(
message="Fin du boost {}. Arrêt de la pompe.".format(self.get_state(self.args['boost_selector_sensor_id'])),
tag="pool_boost")
else:
self.log("As boost %s end during normal filtration time, pump stopping is canceled", self.get_state(self.args['boost_selector_sensor_id']), level = "INFO")
self._send_notification(
message="Fin du boost {}. La pompe n'est pas arrêté car c'est la période de filtration normale.".format(self.get_state(self.args['boost_selector_sensor_id'])),
tag="pool_boost")
self.select_option(entity_id=self.args['boost_selector_sensor_id'], option="Aucun")
# Return boolean depending if now time is between start and stop time of each slots
# ---------------------------------------------------------------------------------
def _is_during_slot(self) -> bool:
if self.now_is_between(self.get_state(self.args['times'][0]['start_time_id']), self.get_state(self.args['times'][0]['stop_time_id'])) or self.now_is_between(self.get_state(self.args['times'][1]['start_time_id']), self.get_state(self.args['times'][1]['stop_time_id'])):
return True
else:
return False
# Detect if boost is in progress
# ------------------------------
def _boost_in_progress(self) -> bool:
if self.get_state(self.args['boost_timer_sensor_id']) == "active":
return True
else:
return False
# Send notification via Home Assistant
# ------------------------------------
def _send_notification(self, **kwargs) -> None:
self.fire_event("NOTIFIER",
action = "send_to_marc",
title = "💦 Temps de filtration piscine",
message = kwargs['message'],
tag = kwargs['tag'])
# Send End Filtration notification
# --------------------------------
def _end_notification(self, **kwargs) -> None:
filtration_duration = self._get_filtration_duration() # Duration in minutes
filtration_done = float(self.get_state(self.args['pump_stats_sensor_id'])) * 60 # Duration in minutes
filtration_percent = (100 / filtration_duration) * filtration_done
messages = []
messages.append(
"La piscine a était filtrée durant {} sur {} soit {}%".format(
timedelta(minutes=filtration_done),
timedelta(minutes=filtration_duration),
str(round(filtration_percent))
)
)
if self._boost_in_progress() is True:
boost_type = self.get_state(self.args['boost_selector_sensor_id'])
boost_end_date = datetime.strptime(self.get_state(self.args['boost_timer_sensor_id'], attribute="finishes_at"), "%Y-%m-%dT%H:%M:%S%z")
now = datetime.now().astimezone()
humanize.i18n.activate("fr_FR")
remaining_boost = humanize.precisedelta(boost_end_date.astimezone() - now, minimum_unit="minutes")
messages.append(
"Un boost de {} est en cours et il reste {} de filtration (Fin : {})".format(
boost_type,
remaining_boost,
boost_end_date.astimezone().strftime("%d-%m-%Y %H:%M")
)
)
self._send_notification(message='. '.join(map(str, messages)), tag="pool_end_filtration")
# List existing Handlers
# ----------------------
def _list_all_handler(self) -> None:
all_handlers = self.get_scheduler_entries()['pool_pump_manager']
for handler in all_handlers:
self.log("Existing Handlers => Date : %s / repeat : %s / interval : %s / callback : %s / kwargs : %s",
all_handlers[handler]['timestamp'],
all_handlers[handler]['repeat'],
all_handlers[handler]['interval'],
all_handlers[handler]['callback'],
all_handlers[handler]['kwargs'],
level="DEBUG")
Je ne vais pas détailler l’ensemble de l’app, mais dans les grandes lignes, elle gère plusieurs choses :
- La surcharge du temps de filtration recommandé par iopool avec un minimum et maximum de filtration (cf ci-dessous dans la configuration de l’app)
- 2 slots de filtration avec un pourcentage de repartition (dans l’exemple de configuration ci-dessous, 40% en slot1 et 60% en slot2)
- La gestion des boosts sur une durée au choix entre 1H, 4H, 8H, 24H
- Une notification en fin de filtration (j’y reviendrais dans le prochain chapitre)
On va déclarer le fichier de log de appdaemon pour l’application qu’on vient de créer. Dans le fichier appdaemon.yaml
present dans /config/appdaemon
avec ce contenu à ajouter dans la partie log :
notifier:
name: notifier
filename: /config/appdaemon/logs/notifier.log
Et redémarrer l’addon AppDeamon
Les logs intéressant en cas de besoin sont
appdaemon.log
,error.log
etpool_pump_manager.log
Maintenant que l’app est créé, il faut faire une configuration qui va l’activer dans le fichier /config/appdaemon/apps/apps.yaml
:
pool_pump_manager:
module: pool_pump_manager
class: pool_pump_manager
use_dictionary_unpacking: true
log: pool_pump_manager
log_level: DEBUG
pump_sensor_id: switch.pool_switch # Le switch qui gère votre pompe de piscine
pump_stats_sensor_id: sensor.temps_de_filtration_ecoule # l'history_stats qui retourne le temps de filtration écoulé (créé dans le chapitre 1)
pool_filtration_duration_sensor_id: sensor.iopool_filtration_time_recommandation # Le sensor avec la recommandation de filtration d'iopool (créé dans le chapitre 1)
pool_filtration_duration_storage: input_number.filtration_duration_calculated # Le sensor qui stocke la durée réelle de filtration a effectué basé sur la recommandation iopool et le min et max ci-dessous
pool_filtration_duration_min: 240 # Optionnel
#pool_filtration_duration_max: 150 # Optionnel
mode_sensor_id: input_select.pool_mode # Le mode défini pour la filtration (créé dans le chapitre 1)
times:
- name: slot1
start_time_id: input_datetime.pool_start_slot1 # L'heure de démarrage du slot 1 de filtration (créé dans le chapitre 1)
stop_time_id: input_datetime.pool_stop_slot1 # L'heure de fin du slot 1 de filtration (calculé par l'app est stocké dans ce sensor) - (créé dans le chapitre 1)
duration_percent: 0.4 # La répartition entre les 2 slots (ici 40% sur le slot 1)
- name: slot2
start_time_id: input_datetime.pool_start_slot2 # L'heure de démarrage du slot 2 de filtration (créé dans le chapitre 1)
stop_time_id: input_datetime.pool_stop_slot2 # L'heure de fin du slot 2 de filtration (calculé par l'app est stocké dans ce sensor) - (créé dans le chapitre 1)
duration_percent: 0.6 # La répartition entre les 2 slots (ici 60% sur le slot 2)
winter:
- name: winter_filtration
duration_minutes: 120 # Temps de filtration en mode Active (hivernage actif)
start_time: "02:00:00" # Heure de démarrage de la filtration (ici 2h du matin)
boost_selector_sensor_id: input_select.pool_boost # Le selecteur de boost (créé dans le chapitre 1)
boost_timer_sensor_id: timer.pool_boost # Le timer du temps restant au boost en cours (créé dans le chapitre 1)
Il vous faut maintenant, configuré les heures de démarrage de la filtration pour le slot1 et slot2 respectivement dans les input_datetime.pool_start_slot1
et input_datetime.pool_start_slot2
a partir des outils de développement ou dans les helpers (Paramètres > Appareils et services > Entrées)
Il faut aussi savoir, que les heures de démarrage des slots de filtration sont fixes mais pas les fins. La fin du slot1 est calculé par rapport au pourcentage du temps de filtration appliqué dans le configuration, mais pour le slot2, c’est selon le temps restant de filtration.
Dans si la sonde iopool augmente son temps de filtration recommandé entre le slot1 et le slot2, le slot2 le prendra en compte dans la durée de filtration qu’il va devoir effectuer.
Une fois que vous aurez validé le fonctionnement de l’app, vous pouvez modifier dans
apps.yaml
le log level pour mettre WARNING.
Les log_level supportés sont :INFO
,WARNING
,ERROR
,CRITICAL
,DEBUG
,NOTSET
.
Etape 3 : Les notifications
L’app ajouté dans le chapitre précédent, envoi en fin de filtration et en fin de boost une notification.
Pour les notifications, j’utilisais déjà la super app AppDaemon de Horizon Domotique qui est très bien expliqué en video : https://www.youtube.com/watch?v=chJylIK0ASo et disponible sur son GitHub.
Etape 4 : L’affichage
J’ai l’habitude de renommer mes entity_id car j’aime avoir mes nom en français mais le fait que HA utilise le nom pour générer les entity_id me gêne un peu.
Donc les entity_id utilisé dans les cartes, sont à ajuster.
Voir ce post qui en parle : [TUTO] - Gestion de sa piscine avec sonde iopool - #31 par mguyard
Maintenant, il faut nous donner de la visibilité sur l’ensemble. J’ai donc ajouté tout cela dans l’ordre :
- Le mode de la sonde de la piscine
- Si des actions sont demandé par la sonde
- L’interrupteur de la pompe
- Le mode de filtration
- Les boosts
- Le temps de filtration (calculé sur la base du min/max et la recommandation iopool)
- Le temps de filtration effectué
- Les horaires de filtration
- La température de la sonde
- Les données de piscine (basé sur le super travail de @Wilsto : GitHub - wilsto/pool-monitor-card: The "Pool Monitor Card" is a home assistant plugin that display information of 1 to 12 pre-defined sensors of your swimming pool : temperature, pH, ORP levels and TDS but also if you need them : salinity, CYA, calcium, phosphate, alkalinity, filter pressure , free chlorine, total chlorine)
En dehors de la card de @Wilsto, j’utilise les card mushroom disponible et dont l’installation est expliqué ici : GitHub - piitaya/lovelace-mushroom: Build a beautiful Home Assistant dashboard easily
Voici ce que cela donne :
Et le code YAML associé :
square: false
type: grid
cards:
- type: custom:mushroom-title-card
title: Piscine
alignment: center
title_tap_action:
action: none
subtitle_tap_action:
action: none
- type: horizontal-stack
cards:
- type: custom:mushroom-entity-card
entity: sensor.iopool_mode
- type: custom:mushroom-entity-card
entity: binary_sensor.iopool_actions_need
name: Actions Requises
icon_color: red
- type: custom:mushroom-entity-card
entity: switch.pool_switch
icon_color: green
icon: mdi:pump
secondary_info: last-changed
- type: horizontal-stack
cards:
- type: custom:mushroom-select-card
entity: input_select.pool_mode
fill_container: true
name: Mode de Filtration
- type: custom:mushroom-select-card
entity: input_select.pool_boost
name: Boost
fill_container: false
- type: horizontal-stack
cards:
- type: custom:mushroom-template-card
primary: Temps Filtration
secondary: >
{% if states('sensor.iopool_filtration_time_recommandation') !=
states(entity) %}
{{ (states(entity)|int * 60)|timestamp_custom('%H:%M', false) }} (iopool: {{ (states('sensor.iopool_filtration_time_recommandation')|int * 60)|timestamp_custom('%H:%M', false) }} )
{% else %}
{{ (states(entity)|int * 60)|timestamp_custom('%H:%M', false) }}
{% endif %}
icon: mdi:clock-check-outline
icon_color: blue
multiline_secondary: true
fill_container: false
tap_action:
action: more-info
hold_action:
action: none
double_tap_action:
action: none
entity: input_number.filtration_duration_calculated
- type: custom:mushroom-template-card
primary: Filtration effectuée
secondary: '{{ timedelta(hours=states(entity) | float(0)) }}'
icon: mdi:chart-line
entity: sensor.temps_de_filtration_ecoule
fill_container: true
icon_color: blue
tap_action:
action: more-info
hold_action:
action: more-info
double_tap_action:
action: more-info
multiline_secondary: false
- type: horizontal-stack
cards:
- type: custom:mushroom-template-card
primary: Horaires de filtration
secondary: >
{{ "- Slot 1 : " +
states('input_datetime.pool_start_slot1').split(':')[:-1] | join(':')
+ " / " + states('input_datetime.pool_stop_slot1').split(':')[:-1] |
join(':')}}
{{ "- Slot 2 : " +
states('input_datetime.pool_start_slot2').split(':')[:-1] | join(':')
+ " / " + states('input_datetime.pool_stop_slot2').split(':')[:-1] |
join(':')}}
icon: mdi:clock
icon_color: blue
multiline_secondary: true
fill_container: false
tap_action:
action: none
hold_action:
action: none
double_tap_action:
action: none
layout: vertical
- type: custom:mini-graph-card
entities:
- entity: sensor.iopool_temperature
name: Température iopool
hours_to_show: 96
animate: true
line_width: 5
group_by: hour
state_adaptive_color: true
hour24: true
decimals: 1
show:
extrema: true
average: true
labels: false
color_thresholds:
- value: 20
color: '#44739e'
- value: 24
color: '#12f33f'
- value: 30
color: '#f39c12'
- value: 32
color: '#c0392b'
- type: custom:pool-monitor-card
title: Données de piscine
temperature: sensor.iopool_temperature
ph: sensor.iopool_ph
orp: sensor.iopool_orp
show_labels: true
language: fr
columns: 1
Ce tuto est plutôt long et pas si simple a expliqué pour tout les niveaux d’utilisateurs HA donc si vous avez des questions ou des remarques, n’hésitez pas.
Merci