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

sensor.hilo_defi change de status avec un délais(retard) #320

Closed
arsenicks opened this issue Nov 22, 2023 · 25 comments
Closed

sensor.hilo_defi change de status avec un délais(retard) #320

arsenicks opened this issue Nov 22, 2023 · 25 comments
Labels
enhancement New feature or request performance Performance

Comments

@arsenicks
Copy link
Collaborator

arsenicks commented Nov 22, 2023

Version of the custom_component

v2023.11.1

Configuration

Config:
image

C'est pas super, j'ai maintenant activé le debug mais voici ce que j'ai comme logs depuis hier midi.

Home Assistant Core
check_tarif: Unable to find state for sensor.hilo_energy_total_low
07:56:06 – (WARNING) Hilo (custom integration) - message first occurred at 21 November 2023 at 17:03:12 and shows up 2675 times
State attributes for sensor.recompenses_hilo exceed maximum size of 16384 bytes. This can cause database performance issues; Attributes will not be stored
07:15:46 – (WARNING) Recorder - message first occurred at 20 November 2023 at 22:16:23 and shows up 94 times
Refreshing websocket token https://api.hiloenergie.com/Events/Notifications/Locations/6411
07:07:41 – (WARNING) Hilo (custom integration) - message first occurred at 20 November 2023 at 23:16:46 and shows up 30 times
Delaying invoke SubscribeToLocation 0 [6411]: Websocket not ready.
02:09:12 – (WARNING) Hilo (custom integration) - message first occurred at 21 November 2023 at 12:30:41 and shows up 5 times
Invalid credentials? Refreshing websocket infos
02:08:45 – (WARNING) Hilo (custom integration) - message first occurred at 21 November 2023 at 12:30:35 and shows up 4 times
Unable to connect to WS server 503, message='Invalid response status', url=URL('https://sigr-domo-prod-01.service.signalr.net/client/?hub=devicehub&asrs.op=%2FDeviceHub&asrs_request_id=REQIDREMOVED&access_token=TOKENREMOVED')
02:08:45 – (ERROR) Hilo (custom integration) - message first occurred at 21 November 2023 at 12:30:30 and shows up 5 times
Connection was closed.
02:08:40 – (ERROR) /usr/local/lib/python3.11/site-packages/pyhilo/websocket.py - message first occurred at 21 November 2023 at 12:30:25 and shows up 4 times
Received close event from SignalR: Error: CLOSE Target: Args: Error: Service reloading, please reconnect.
02:08:40 – (ERROR) Hilo (custom integration) - message first occurred at 21 November 2023 at 12:30:25 and shows up 12 times
Unhandled websocket event: WebsocketEvent(event_type_id=7, target='', arguments='', invocation=None, error='Service reloading, please reconnect.', timestamp=datetime.datetime(2023, 11, 20, 22, 16, 11, 277414), event_type='CLOSE')
02:08:40 – (WARNING) Hilo (custom integration) - message first occurred at 21 November 2023 at 12:30:25 and shows up 4 times
Refreshing websocket token https://automation.hiloenergie.com/DeviceHub/negotiate
02:08:10 – (WARNING) Hilo (custom integration)
Refreshing websocket token https://api.hiloenergie.com/Automation/v1/api/DRMS/Locations/6411/Seasons
21 November 2023 at 19:04:08 – (WARNING) Hilo (custom integration)
Error doing job: Task exception was never retrieved
21 November 2023 at 12:30:30 – (ERROR) Hilo (custom integration)

Describe the bug

Selon ma config, l'intégration devrait déclencher la période d'appréciation 4h avant le pre-heat. Donc minuit normalement le sensor.hilo_defi change pour appreciation et déclenche mes automatismes.

Le premier défi la nuit passé j'ai du déclencher mes automatismes moi même à 00:10 car le sensor hilo_defi ne changeait pas. Pourtant dans les attributs je vois bien que l'attribute appreciation_start et end à la bonne valeur. Le sensor hilo_defi à finalement changé de status pour appreciation mais seulement à 00:35, ensuite pre_heat en retard aussi, même chose pour reduction. Les heures sont dans les captures d'écran

Debug log

Je n'ai que des captures d'écran et les logs plus haut, j'ai activé le debug pour le prochain défi.

Screenshot 2023-11-22 07 45 21
Screenshot 2023-11-22 07 44 55
Screenshot 2023-11-22 07 39 24
Screenshot 2023-11-22 07 39 17

@FrancoLoco
Copy link
Collaborator

FrancoLoco commented Nov 22, 2023

Meme chose ici, mais il me semble que c'était comme ca l'année passée aussi non? Le délai est à cause d'Hilo je crois.. ou c'est peut-être un poll time trop long qui devrait être raccourci pendant les défis pour être sur d'avoir toujours le bon état de sensor.défi.. ou en fait ca pourrait même être hardcodé car les heures sont toujours les mêmes...

image

Mais bref moi ca fait un bout que je fais fonctionner mes automatisations avec des heures et non des changements d'états du sensor défi car sinon ca fait ce que tu rapportes parfois..

@arsenicks
Copy link
Collaborator Author

arsenicks commented Nov 22, 2023

Meme chose ici, mais il me semble que c'était comme ca l'année passée aussi non? Le délai est à cause d'Hilo je crois.. ou c'est peut-être un poll time trop long qui devrait être raccourci pendant les défis pour être sur d'avoir toujours le bon état de sensor.défi.. ou en fait ca pourrait même être hardcodé car les heures sont toujours les mêmes...

image

Mais bref moi ca fait un bout que je fais fonctionner mes automatisations avec des heures et non des changements d'états du sensor défi car sinon ca fait ce que tu rapportes parfois..

Ouin je vais peut être changer pour ca aussi.. J'aimais bien la simplicité du changement d'état du sensor.. J'ai le souvenir qu'il y avait parfois un délais l'an passé mais plus dans les 5-6mins que 30!

Ca fais une éternité que je n'ai pas regardé le code, mais je ne vois pas trop de raison pour que ce soit relié à un poll time ou à hilo puisque l'intégration sait déja qu'il y a un défi de prévu, la phase d'appréciation est seulement codé dans la logique du component et non quelque chose qu'on ramasse de hilo right ? Car quand on regarde les attribut dans les screenshot on vois bien que les heures de debut et fin de la phase d'appréciation sont bonne et étaient la depuis le moment ou le sensor à changé de off à scheduled

edit: Nevermind, je réalise que tout est décallé, pas seulement l'appréciation alors c'est peut être relié à un call chez hilo, aucune idée. Je suis disponible pour tester ou autre si besoin.

@ic-dev21
Copy link
Collaborator

Je confirme la même chose

@nlz242
Copy link
Contributor

nlz242 commented Nov 23, 2023

J'ai augmenté le setting de polling par défaut, ça peut probablement expliquer le délais supplémentaire versus l'an passé.
Étant donné qu'on a les données dans l'intégration, on pourrait peut-etre mettre un timer qui check en mémoire, à chaque minute par exemple, dans le sensor, pour remonter les valeurs ? Une idée comme ça.

@ic-dev21
Copy link
Collaborator

J'ai augmenté le setting de polling par défaut, ça peut probablement expliquer le délais supplémentaire versus l'an passé.
Étant donné qu'on a les données dans l'intégration, on pourrait peut-etre mettre un timer qui check en mémoire, à chaque minute par exemple, dans le sensor, pour remonter les valeurs ? Une idée comme ça.

Tu parles d'ici?

EVENT_SCAN_INTERVAL = 3000

@nlz242
Copy link
Contributor

nlz242 commented Nov 23, 2023

Exact. Avant c'était 600 (donc 10 minutes).
Les 2 sensors (HiloChallengeSensor et HiloRewardSensor), font du polling sur les mêmes API...
Je vais regarder si il y'aurait possibilité de faire quelque chose pour faire du polling sur l'API en dehors des sensors et faire en sorte que le HiloChallengeSensor s'update plus souvent, mais à partir des données qui aurait été chargé en dehors du sensor. On pourrait avoir le sensor avec un délais super bas en faisant ça, sans pour autant bombarder les API pour rien.

@arsenicks
Copy link
Collaborator Author

Est-ce qu'on a vraiment besoin de faire du polling pour changer le status de ce sensor ? Je veux dire, une fois qu'il passe de off a scheduled, tout les attribut du sensor contienne déja les dates et heures de début de toutes les phases. On pourrais simplement utiliser ces valeurs pour changer le status du sensor ensuite ?

@ic-dev21
Copy link
Collaborator

Est-ce qu'on a vraiment besoin de faire du polling pour changer le status de ce sensor ? Je veux dire, une fois qu'il passe de off a scheduled, tout les attribut du sensor contienne déja les dates et heures de début de toutes les phases. On pourrais simplement utiliser ces valeurs pour changer le status du sensor ensuite ?

J'aime l'idée, elle serait fiable même avec une perte d'internet, mais comment on ferait pour forcer le changement de state dans HA? Via des automatisations? C'est ce bout-là qui me tanne un peu.

@arsenicks
Copy link
Collaborator Author

J'aime l'idée, elle serait fiable même avec une perte d'internet, mais comment on ferait pour forcer le changement de state dans HA? Via des automatisations? C'est ce bout-là qui me tanne un peu.

J'ignore comment ca peu se faire dans le component malheureusement mais si on peu coder un polling time pour appeler l'api et modifié le state selon la réponse, j'imagine qu'on pourrais coder d'utiliser ses propres atributs pour s'updater. Mais bon, tout à l'air facile et simple quand c'est pas toi qui code alors jvais vous laisser juger de tout ca :P

@ic-dev21
Copy link
Collaborator

J'aime l'idée, elle serait fiable même avec une perte d'internet, mais comment on ferait pour forcer le changement de state dans HA? Via des automatisations? C'est ce bout-là qui me tanne un peu.

J'ignore comment ca peu se faire dans le component malheureusement mais si on peu coder un polling time pour appeler l'api et modifié le state selon la réponse, j'imagine qu'on pourrais coder d'utiliser ses propres atributs pour s'updater. Mais bon, tout à l'air facile et simple quand c'est pas toi qui code alors jvais vous laisser juger de tout ca :P

J'ignore aussi comment le faire, je suis juste un gars qui s'essaye pis qui est chanceux des fois parce que ça marche haha. Peut-être que @valleedelisle aurait une solution élégante en tête.

@ic-dev21
Copy link
Collaborator

Exact. Avant c'était 600 (donc 10 minutes).
Les 2 sensors (HiloChallengeSensor et HiloRewardSensor), font du polling sur les mêmes API...
Je vais regarder si il y'aurait possibilité de faire quelque chose pour faire du polling sur l'API en dehors des sensors et faire en sorte que le HiloChallengeSensor s'update plus souvent, mais à partir des données qui aurait été chargé en dehors du sensor. On pourrait avoir le sensor avec un délais super bas en faisant ça, sans pour autant bombarder les API pour rien.

Peut-être domper le tout dans un yaml comme le token de login et relire dedans?

@RayLation
Copy link

RayLation commented Nov 25, 2023

salut gang,

D'ici à ce que vous corrigiez direct dans l'intégration, voici un workaround pour contouner le lag que je me suis codé l'an passé. Ca a fonctionné très bien toute l'année passée et cette année aussi. Ca crée tout simplement un 2ième sensor (sensor.defi_hilo_phase) que vous pouvez utiliser pour déclencher/trigger vos automations. Ca se base directement sur les heures qui sont dans sensor.defi_hilo pour changer l'état du sensor, ainsi on élimine le lag complètement.

J'ai ajouté qq éléments aussi pour le rendre "meilleur" (selon moi). Pour un défi du matin, la phase réduction va partir 5min avant et finir 5min après (c'est paramétrable via le "timedelta(minutes=5)"). C'est probablement pas nécessaire, mais je voulais être sûr que mes affaires soient bien arrêtés avant que le défi parte chez hilo.

Également, j'ai ajouté une phase "boost" durant laquelle j'ai une scène qui laisse descendre la température un peu, afin de booster la phase d'appréciation (et donc booster les rewards). Dans mon cas, ca dure 2h avant la phase appréciation. C'est également paramétrables (via le bout timedelta(hours=2))

À noter que l'ensemble des attributs qui sont dans sensor.defi_hilo_phase ne sont pas recopiés dans mon sensor. C'est vraiment juste l'état.

Faites-moi signe si vous trouvez des bugs!

À mettre dans votre sensors.yaml (ou configuration.yaml si vous n'avez pas sensors.yaml)


  - platform: template

    sensors:

      defi_hilo_phase:
        friendly_name: Phase Défi en cours
        value_template: >
          {% if (states('sensor.defi_hilo') in ["unavailable","unknown"]) %}
            {{states('sensor.defi_hilo')}}

          {% elif (state_attr('sensor.defi_hilo','next_events')|count > 0) %}
            {% set appreciation_start = state_attr('sensor.defi_hilo','next_events')[0].phases.appreciation_start %}  
            {% set preheat_start = state_attr('sensor.defi_hilo','next_events')[0].phases.preheat_start %}  
            {% set reduction_start = state_attr('sensor.defi_hilo','next_events')[0].phases.reduction_start %}  
            {% set recovery_start = state_attr('sensor.defi_hilo','next_events')[0].phases.recovery_start %}  
            {% set recovery_end = state_attr('sensor.defi_hilo','next_events')[0].phases.recovery_end %}  

            {% if (now() > recovery_end) %}
              {% if (state_attr('sensor.defi_hilo','next_events')|count > 1) %}
                scheduled
              {% else %}
                off
              {% endif %}
            {% elif (now() > recovery_start + timedelta(minutes=5)) %}
              recovery
            {% elif (now() > reduction_start - timedelta(minutes=5)) %}
              reduction
            {% elif (now() > preheat_start) %}
              pre_heat
            {% elif (now() > appreciation_start) %}
              appreciation
            {% elif (now() > appreciation_start - timedelta(hours=2)) %}
              boost
            {% else %}
              scheduled
            {% endif %}
          {% else %}
            off
          {% endif %}

Exemple pour le défi de mercredi:

SEnsor normal:
image

Mon sensor:
image

a+

@ic-dev21
Copy link
Collaborator

salut gang,

D'ici à ce que vous corrigiez direct dans l'intégration, voici un workaround pour contouner le lag que je me suis codé l'an passé. Ca a fonctionné très bien toute l'année passée et cette année aussi. Ca crée tout simplement un 2ième sensor (sensor.defi_hilo_phase) que vous pouvez utiliser pour déclencher/trigger vos automations. Ca se base directement sur les heures qui sont dans sensor.defi_hilo pour changer l'état du sensor, ainsi on élimine le lag complètement.

J'ai ajouté qq éléments aussi pour le rendre "meilleur" (selon moi). Pour un défi du matin, la phase réduction va partir 5min avant et finir 5min après (c'est paramétrable via le "timedelta(minutes=5)"). C'est probablement pas nécessaire, mais je voulais être sûr que mes affaires soient bien arrêtés avant que le défi parte chez hilo.

Également, j'ai ajouté une phase "boost" durant laquelle j'ai une scène qui laisse descendre la température un peu, afin de booster la phase d'appréciation (et donc booster les rewards). Dans mon cas, ca dure 2h avant la phase appréciation. C'est également paramétrables (via le bout timedelta(hours=2))

À noter que l'ensemble des attributs qui sont dans sensor.defi_hilo_phase ne sont pas recopiés dans mon sensor. C'est vraiment juste l'état.

Faites-moi signe si vous trouvez des bugs!

À mettre dans votre sensors.yaml (ou configuration.yaml si vous n'avez pas sensors.yaml)


  - platform: template

    sensors:

      defi_hilo_phase:
        friendly_name: Phase Défi en cours
        value_template: >
          {% if (states('sensor.defi_hilo') in ["unavailable","unknown"]) %}
            {{states('sensor.defi_hilo')}}

          {% elif (state_attr('sensor.defi_hilo','next_events')|count > 0) %}
            {% set appreciation_start = state_attr('sensor.defi_hilo','next_events')[0].phases.appreciation_start %}  
            {% set preheat_start = state_attr('sensor.defi_hilo','next_events')[0].phases.preheat_start %}  
            {% set reduction_start = state_attr('sensor.defi_hilo','next_events')[0].phases.reduction_start %}  
            {% set recovery_start = state_attr('sensor.defi_hilo','next_events')[0].phases.recovery_start %}  
            {% set recovery_end = state_attr('sensor.defi_hilo','next_events')[0].phases.recovery_end %}  

            {% if (now() > recovery_end) %}
              {% if (state_attr('sensor.defi_hilo','next_events')|count > 1) %}
                scheduled
              {% else %}
                off
              {% endif %}
            {% elif (now() > recovery_start + timedelta(minutes=5)) %}
              recovery
            {% elif (now() > reduction_start - timedelta(minutes=5)) %}
              reduction
            {% elif (now() > preheat_start) %}
              pre_heat
            {% elif (now() > appreciation_start) %}
              appreciation
            {% elif (now() > appreciation_start - timedelta(hours=2)) %}
              boost
            {% else %}
              scheduled
            {% endif %}
          {% else %}
            off
          {% endif %}

Exemple pour le défi de mercredi:

SEnsor normal: image

Mon sensor: image

a+

Merci!

J’ai checké la doc rapidos en dînant et je suis tombé là dessus, il serait peut-être utilisable tel quel:

https://developers.home-assistant.io/blog/2020/11/09/system-health-and-templates

@ic-dev21
Copy link
Collaborator

Premiers essais infructueux:

class HiloChallengeSensor2(HiloEntity, TrackTemplate):
    """Hilo challenge sensor.
    Its state will be either:
    - off: no ongoing or scheduled challenge
    - scheduled: A challenge is scheduled, details in the next_events
                 extra attribute
    - pre_heat: Currently in the pre-heat phase
    - reduction or on: Challenge is currently active, heat is lowered
    - recovery: Challenge is completed, we're reheating.
    """

    def __init__(self, hilo, scan_interval):
        self._attr_name = "Defi Hilo Raylation"
        super().__init__(hilo, name=self._attr_name, device=device)
        self._attr_unique_id = slugify(self._attr_name)
        LOG.debug(f"Setting up ChallengeSensor2 entity: {self._attr_name}")
        self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL)
        self._state = template
        self.async_update = Throttle(self.scan_interval)(self._async_update)
        
    @property
    def state(self):
        return self._state

    template = {{
      {% if (states('sensor.defi_hilo') in ["unavailable","unknown"]) %}
        {{states('sensor.defi_hilo')}}

      {% elif (state_attr('sensor.defi_hilo','next_events')|count > 0) %}
        {% set appreciation_start = state_attr('sensor.defi_hilo','next_events')[0].phases.appreciation_start %}  
        {% set preheat_start = state_attr('sensor.defi_hilo','next_events')[0].phases.preheat_start %}  
        {% set reduction_start = state_attr('sensor.defi_hilo','next_events')[0].phases.reduction_start %}  
        {% set recovery_start = state_attr('sensor.defi_hilo','next_events')[0].phases.recovery_start %}  
        {% set recovery_end = state_attr('sensor.defi_hilo','next_events')[0].phases.recovery_end %}  

        {% if (now() > recovery_end) %}
          {% if (state_attr('sensor.defi_hilo','next_events')|count > 1) %}
            scheduled
          {% else %}
            off
          {% endif %}
        {% elif (now() > recovery_start + timedelta(minutes=5)) %}
          recovery
        {% elif (now() > reduction_start - timedelta(minutes=5)) %}
          reduction
        {% elif (now() > preheat_start) %}
          pre_heat
        {% elif (now() > appreciation_start) %}
          appreciation
        {% elif (now() > appreciation_start - timedelta(hours=2)) %}
          boost
        {% else %}
          scheduled
        {% endif %}
      {% else %}
        off
      {% endif %} }}

        async_track_template_result(
            hass,
            [TrackTemplate(template, None)],
            lambda event, updates: print(event, updates),
        )
    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()

Étrangement quand je rajoute cette classe là je plante le reste des sensors sur mon environnement de test... Je laisse ça ici d'un coup que quelqu'un serait en mesure de déjamboniser ça!

@RayLation
Copy link

RayLation commented Nov 27, 2023

Salut!

J'avoue que je ne comprends pas trop ce que tu essaies de faire... tu veux que l'intégration créée automatiqueemnt le template sensor ? Je ne vois pas l'utilité vraiment... -pourquoi ne pas le mettre dans sensors.yaml tout simplement?

Il me semble que tant qu'à vouloir améliorer l'intégration, il serait plus pertinent de la modifier afin que le vrai sensor fonctionne bien sans lag. Je pense que le meilleur moyen serait de modifier la fonction _async_update. Il faudrait qu'il y ait 2 "scan interval":

  • Interval 1: (Toutes les 60 secondes mettons) --> on poll les données de hilo et on met à jour le state ainsi que les autres attributs. C'est déjà ce que fait la fonction.
  • Interval 2: (Toutes les 1 ou 5 secondes mettons) --> on ne poll PAS hilo. On met à jour seulement le state, basé sur les heures qui sont déjà contenues dans le sensor. (équivalent de ce que fait le template sensor additionnel, mais de facon beaucoup plus élégante)

Si qqn est capable de m'expliquer comment je peux me monter facilement un environnement de DEV, je peux le coder moi-même...

@ic-dev21
Copy link
Collaborator

ic-dev21 commented Nov 27, 2023

Salut!

J'avoue que je ne comprends pas trop ce que tu essaies de faire... tu veux que l'intégration créée automatiqueemnt le template sensor ? Je ne vois pas l'utilité vraiment... -pourquoi ne pas le mettre dans sensors.yaml tout simplement?

Il me semble que tant qu'à vouloir améliorer l'intégration, il serait plus pertinent de la modifier afin que le vrai sensor fonctionne bien sans lag. Je pense que le meilleur moyen serait de modifier la fonction _async_update. Il faudrait qu'il y ait 2 "scan interval":

  • Interval 1: (Toutes les 60 secondes mettons) --> on poll les données de hilo et on met à jour le state ainsi que les autres attributs. C'est déjà ce que fait la fonction.
  • Interval 2: (Toutes les 1 ou 5 secondes mettons) --> on ne poll PAS hilo. On met à jour seulement le state, basé sur les heures qui sont déjà contenues dans le sensor. (équivalent de ce que fait le template sensor additionnel, mais de facon beaucoup plus élégante)

Si qqn est capable de m'expliquer comment je peux me monter facilement un environnement de DEV, je peux le coder moi-même...

Je me disais (naïvement) que si c'était généré automatiquement ça serait mieux pour l'utilisateur qui veut juste un module plug and play sans trop se casser la tête. La doc de HA que j'ai linké semblait montrer qu'un template pouvait être planté dans le code.

Je trouvais ça plus élégant de le créer d'emblée, dans ma tête. J'éssayais d'éviter de poller Hilo plus que nécessaire.

Sinon est-ce que faire quelque chose du style:

    async def _async_update(self):
        new_events = []
        events = await self._hilo._api.get_gd_events(self._hilo.devices.location_id)
        LOG.debug(f"Events received from Hilo: {events}")
        for raw_event in events:
            details = await self._hilo._api.get_gd_events(
                self._hilo.devices.location_id, event_id=raw_event["id"]
            )
            event = Event(**details)
            if self._hilo.appreciation > 0:
                event.appreciation(self._hilo.appreciation)
            new_events.append(event.as_dict())
        self._state = "off"
        self._next_events = []
        if len(new_events):
            self._state = new_events[0]["state"]
            self._next_events = new_events
            self.scan_interval = timedelta(seconds=60)    

Pourrait marcher comme patch temporaire?

Pour ta question d'environnement de dev, mon HA roule en docker sur un RPI4. J'ai un NAS Synology aussi donc j'ai spinné un docker HA dessus pis je gosse dans le code de celui-là.

Je ne suis pas un dev pour 2 cennes, j'ai appris le peu que je sais de python en gossant sur le code de ce custom component-ci. Je suis toujours willing d'en apprendre de meilleur que moi.

@RayLation
Copy link

RayLation commented Nov 29, 2023

voici ce que ca donnerait pour la class HiloChallengeSensor. Je pense que ca fait du sens, mais c'est 0 testé.... Faudrait attendre un vrai défi, à moins qu'on soit capable de faker un défi pour voir si ca passe...

En gros, la logique est directement dans la property state, laquelle se base sur les attributs du sensor qu'il a récupéré lors du dernier scan.
Quand le scan se fait, on ne met plus à jour le statut. C'est fait dans la property.

Le "pre_cold" est une invention de ma part.... ne pas en tenir compte pour maintenant.

class HiloChallengeSensor(HiloEntity, RestoreEntity, SensorEntity):
    """Hilo challenge sensor.
    Its state will be either:
    - off: no ongoing or scheduled challenge
    - scheduled: A challenge is scheduled, details in the next_events
                 extra attribute
    - pre_heat: Currently in the pre-heat phase
    - reduction or on: Challenge is currently active, heat is lowered
    - recovery: Challenge is completed, we're reheating.
    """

    def __init__(self, hilo, device, scan_interval):
        self._attr_name = "Defi Hilo"
        super().__init__(hilo, name=self._attr_name, device=device)
        self._attr_unique_id = slugify(self._attr_name)
        LOG.debug(f"Setting up ChallengeSensor entity: {self._attr_name}")
        self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL)
        self._state = "off"
        self._next_events = []
        self.async_update = Throttle(self.scan_interval)(self._async_update)

    @property
    def state(self):        
        if len(self._next_events) > 0:
            if datetime.now(timezone.utc) > self._next_events[0].phases.recovery_end:
                if len(self._next_events) > 1: 
                    # another challenge is scheduled after this one
                    return "scheduled"
                else:
                    return "off"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.recovery_start:
                return "recovery"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.reduction_start:
                return "reduction"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.preheat_start:
                return "pre_heat"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.appreciation_start:
                return "appreciation"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.appreciation_start - timedelta(hours = 2):
                return "pre_cold"
            else:
                return "scheduled"
        else:
            return "off"

    @property
    def icon(self):
        if not self._device.available:
            return "mdi:lan-disconnect"
        if self.state == "appreciation":
            return "mdi:glass-cocktail"
        if self.state == "off":
            return "mdi:lightning-bolt"
        if self.state == "scheduled":
            return "mdi:progress-clock"
        if self.state == "pre_heat":
            return "mdi:radiator"
        if self.state in ["reduction", "on"]:
            return "mdi:power-plug-off"
        if self.state == "recovery":
            return "mdi:calendar-check"
        if self.state == "pre_cold":
            return "mdi:radiator-off"
        return "mdi:battery-alert"

    @property
    def should_poll(self):
        return True

    @property
    def extra_state_attributes(self):
        return {"next_events": self._next_events}

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()
        last_state = await self.async_get_last_state()
        if last_state:
            self._last_update = dt_util.utcnow()
            self._state = last_state.state
            self._next_events = last_state.attributes.get("next_events", [])

    async def _async_update(self):
        new_events = []
        events = await self._hilo._api.get_gd_events(self._hilo.devices.location_id)
        LOG.debug(f"Events received from Hilo: {events}")
        for raw_event in events:
            details = await self._hilo._api.get_gd_events(
                self._hilo.devices.location_id, event_id=raw_event["id"]
            )
            event = Event(**details)
            if self._hilo.appreciation > 0:
                event.appreciation(self._hilo.appreciation)
            new_events.append(event.as_dict())
        
        self._next_events = []
        if len(new_events):            
            self._next_events = new_events
            #we don't update the state here anymore, since it's now calculated in the "state"

@ic-dev21
Copy link
Collaborator

voici ce que ca donnerait pour la class HiloChallengeSensor. Je pense que ca fait du sens, mais c'est 0 testé.... Faudrait attendre un vrai défi, à moins qu'on soit capable de faker un défi pour voir si ca passe...

En gros, la logique est directement dans la property state, laquelle se base sur les attributs du sensor qu'il a récupéré lors du dernier scan. Quand le scan se fait, on ne met plus à jour le statut. C'est fait dans la property.

Le "pre_cold" est une invention de ma part.... ne pas en tenir compte pour maintenant.

class HiloChallengeSensor(HiloEntity, RestoreEntity, SensorEntity):
    """Hilo challenge sensor.
    Its state will be either:
    - off: no ongoing or scheduled challenge
    - scheduled: A challenge is scheduled, details in the next_events
                 extra attribute
    - pre_heat: Currently in the pre-heat phase
    - reduction or on: Challenge is currently active, heat is lowered
    - recovery: Challenge is completed, we're reheating.
    """

    def __init__(self, hilo, device, scan_interval):
        self._attr_name = "Defi Hilo"
        super().__init__(hilo, name=self._attr_name, device=device)
        self._attr_unique_id = slugify(self._attr_name)
        LOG.debug(f"Setting up ChallengeSensor entity: {self._attr_name}")
        self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL)
        self._state = "off"
        self._next_events = []
        self.async_update = Throttle(self.scan_interval)(self._async_update)

    @property
    def state(self):        
        if len(self._next_events) > 0:
            if datetime.now(timezone.utc) > self._next_events[0].phases.recovery_end:
                if len(self._next_events) > 1: 
                    # another challenge is scheduled after this one
                    return "scheduled"
                else:
                    return "off"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.recovery_start:
                return "recovery"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.reduction_start:
                return "reduction"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.preheat_start:
                return "pre_heat"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.appreciation_start:
                return "appreciation"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.appreciation_start - timedelta(hours = 2):
                return "pre_cold"
            else:
                return "scheduled"
        else:
            return "off"

    @property
    def icon(self):
        if not self._device.available:
            return "mdi:lan-disconnect"
        if self.state == "appreciation":
            return "mdi:glass-cocktail"
        if self.state == "off":
            return "mdi:lightning-bolt"
        if self.state == "scheduled":
            return "mdi:progress-clock"
        if self.state == "pre_heat":
            return "mdi:radiator"
        if self.state in ["reduction", "on"]:
            return "mdi:power-plug-off"
        if self.state == "recovery":
            return "mdi:calendar-check"
        if self.state == "pre_cold":
            return "mdi:radiator-off"
        return "mdi:battery-alert"

    @property
    def should_poll(self):
        return True

    @property
    def extra_state_attributes(self):
        return {"next_events": self._next_events}

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()
        last_state = await self.async_get_last_state()
        if last_state:
            self._last_update = dt_util.utcnow()
            self._state = last_state.state
            self._next_events = last_state.attributes.get("next_events", [])

    async def _async_update(self):
        new_events = []
        events = await self._hilo._api.get_gd_events(self._hilo.devices.location_id)
        LOG.debug(f"Events received from Hilo: {events}")
        for raw_event in events:
            details = await self._hilo._api.get_gd_events(
                self._hilo.devices.location_id, event_id=raw_event["id"]
            )
            event = Event(**details)
            if self._hilo.appreciation > 0:
                event.appreciation(self._hilo.appreciation)
            new_events.append(event.as_dict())
        
        self._next_events = []
        if len(new_events):            
            self._next_events = new_events
            #we don't update the state here anymore, since it's now calculated in the "state"

Merci

Veux-tu que je mette ça sur mon NAS en test pour pas que tu scrap ton prod?

@RayLation
Copy link

oui super.
J'ai fait la même chose sur une VM finalement. J,ai pas réussi à me monter un "dev container" pour débugger, donc on va le faire à la mitaine. Je m'attends à ce qu'il y ait qq tweaks à faire

@ic-dev21
Copy link
Collaborator

ic-dev21 commented Nov 29, 2023

oui super.
J'ai fait la même chose sur une VM finalement. J,ai pas réussi à me monter un "dev container" pour débugger, donc on va le faire à la mitaine. Je m'attends à ce qu'il y ait qq tweaks à faire

Okay, juste pour me garder des notes:

const.py, ajout de:

CONF_PRE_COLD = "pre_cold"
DEFAULT_PRE_COLD = 0

config_flow.py:

from .const import (
    CONF_APPRECIATION_PHASE,
    CONF_CHALLENGE_LOCK,
    CONF_GENERATE_ENERGY_METERS,
    CONF_HQ_PLAN_NAME,
    CONF_LOG_TRACES,
CONF_PRE_COLD,
    CONF_TRACK_UNKNOWN_SOURCES,
    CONF_UNTARIFICATED_DEVICES,
    DEFAULT_APPRECIATION_PHASE,
    DEFAULT_CHALLENGE_LOCK,
    DEFAULT_GENERATE_ENERGY_METERS,
    DEFAULT_HQ_PLAN_NAME,
    DEFAULT_LOG_TRACES,
DEFAULT_PRE_COLD,
    DEFAULT_SCAN_INTERVAL,
    DEFAULT_TRACK_UNKNOWN_SOURCES,
    DEFAULT_UNTARIFICATED_DEVICES,
    DOMAIN,
    LOG,
    MIN_SCAN_INTERVAL,
)

Ensuite:

STEP_OPTION_SCHEMA = vol.Schema(
    {
        vol.Optional(
            CONF_GENERATE_ENERGY_METERS, default=DEFAULT_GENERATE_ENERGY_METERS
        ): cv.boolean,
        vol.Optional(
            CONF_UNTARIFICATED_DEVICES,
            default=DEFAULT_UNTARIFICATED_DEVICES,
        ): cv.boolean,
        vol.Optional(
            CONF_LOG_TRACES,
            default=DEFAULT_LOG_TRACES,
        ): cv.boolean,
        vol.Optional(
            CONF_CHALLENGE_LOCK,
            default=DEFAULT_CHALLENGE_LOCK,
        ): cv.boolean,
        vol.Optional(
            CONF_TRACK_UNKNOWN_SOURCES,
            default=DEFAULT_TRACK_UNKNOWN_SOURCES,
        ): cv.boolean,
        vol.Optional(
            CONF_APPRECIATION_PHASE,
            default=DEFAULT_APPRECIATION_PHASE,
        ): cv.positive_int,
    vol.Optional(
        CONF_PRE_COLD,
        default=DEFAULT_PRE_COLD,
    ): cv.positive_int,
        vol.Optional(CONF_HQ_PLAN_NAME, default=DEFAULT_HQ_PLAN_NAME): cv.string,
        vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): (
            vol.All(cv.positive_int, vol.Range(min=MIN_SCAN_INTERVAL))
        ),
    }
)

Ainsi que:

class HiloOptionsFlowHandler(config_entries.OptionsFlow):
    """Handle a Hilo options flow."""

    def __init__(self, config_entry: ConfigEntry) -> None:
        """Initialize."""
        self.config_entry = config_entry

    async def async_step_init(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Manage the options."""
        if user_input is not None:
            return self.async_create_entry(title="", data=user_input)

        return self.async_show_form(
            step_id="init",
            data_schema=vol.Schema(
                {
                    vol.Optional(
                        CONF_GENERATE_ENERGY_METERS,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_GENERATE_ENERGY_METERS
                            )
                        },
                    ): cv.boolean,
                    vol.Optional(
                        CONF_UNTARIFICATED_DEVICES,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_UNTARIFICATED_DEVICES
                            )
                        },
                    ): cv.boolean,
                    vol.Optional(
                        CONF_LOG_TRACES,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_LOG_TRACES
                            )
                        },
                    ): cv.boolean,
                    vol.Optional(
                        CONF_CHALLENGE_LOCK,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_CHALLENGE_LOCK
                            )
                        },
                    ): cv.boolean,
                    vol.Optional(
                        CONF_TRACK_UNKNOWN_SOURCES,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_TRACK_UNKNOWN_SOURCES
                            )
                        },
                    ): cv.boolean,
                    vol.Optional(
                        CONF_HQ_PLAN_NAME,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_HQ_PLAN_NAME
                            )
                        },
                    ): cv.string,
                    vol.Optional(
                        CONF_APPRECIATION_PHASE,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_APPRECIATION_PHASE
                            )
                        },
                ): cv.positive_int,
                **vol.Optional(
                    CONF_PRE_COLD,
                    description={
                        "suggested_value": self.config_entry.options.get(
                            CONF_PRE_COLD
                        )
                    },
                    ): cv.positive_int,                    
                    vol.Optional(
                        CONF_SCAN_INTERVAL,
                        description={
                            "suggested_value": self.config_entry.options.get(
                                CONF_SCAN_INTERVAL
                            )
                        },
                    ): (vol.All(cv.positive_int, vol.Range(min=MIN_SCAN_INTERVAL))),
                }
            ),
        )

en.json:

  "options": {
    "step": {
      "init": {
        "title": "Configure Hilo",
        "data": {
          "generate_energy_meters": "Generate energy meters",
          "untarificated_devices": "Generate only total meters for each devices",
          "hq_plan_name": "Hydro Quebec rate plan name ('rate d' or 'flex d')",
          "scan_interval": "Scan interval (min: 60s)",
          "log_traces": "Also log request data and websocket messages (requires debug log level on both the integration and pyhilo)",
          "challenge_lock": "Lock climate entities during Hilo challenges, preventing any changes when a challenge is in progress.",
          "track_unknown_sources": "Track unknown power sources in a separate energy sensor. This is a round approximation calculated when we get a reading from the Smart Energy Meter.",
          "appreciation_phase": "Add an appreciation phase of X hours before the preheat phase."
      "pre_cold":"Add a period of time before the appeciation phase to let the temperature lower more"
        }
      }
    }
  }
}

fr.json:

  "options": {
    "step": {
      "init": {
        "title": "Configurer Hilo",
        "data": {
          "generate_energy_meters": "Générer compteurs de consommation électrique",
          "untarificated_devices": "Générer seulement les compteurs totaux pour chaque appareil",
          "hq_plan_name": "Nom du tarif Hydro Québec ('rate d' ou 'flex d')",
          "scan_interval": "Intervalle de mise à jour (min: 60s)",
          "log_traces": "Enregistrer aussi les requêtes et messages websocket (requiert le niveau de journalisation debug sur L'intégration et pyhilo)",
          "challenge_lock": "Vérouiller les entités climate lors de défis Hilo, empêchant tout changement lorsqu'un défi est en cours.",
          "track_unknown_sources": "Suivre des sources de consommation inconnues dans un compteur séparé. Ceci est une approximation calculée à partir de la lecture du compteur intelligent.",
          "appreciation_phase": "Ajouter une période d'appréciation de X heures avant la phase de préchauffage."
      "pre_cold":"Ajout une période de temps pour descendre la température plus bas avant l'appréciation."
        }
      }
    }
  }
}

init.py

from .const import (
    CONF_APPRECIATION_PHASE,
    CONF_CHALLENGE_LOCK,
    CONF_GENERATE_ENERGY_METERS,
    CONF_HIGH_PERIODS,
    CONF_HQ_PLAN_NAME,
    CONF_LOG_TRACES,
CONF_PRE_COLD,
    CONF_TARIFF,
    CONF_TRACK_UNKNOWN_SOURCES,
    CONF_UNTARIFICATED_DEVICES,
    DEFAULT_APPRECIATION_PHASE,
    DEFAULT_CHALLENGE_LOCK,
    DEFAULT_GENERATE_ENERGY_METERS,
    DEFAULT_HQ_PLAN_NAME,
    DEFAULT_LOG_TRACES,
DEFAULT_PRE_COLD,
    DEFAULT_SCAN_INTERVAL,
    DEFAULT_TRACK_UNKNOWN_SOURCES,
    DEFAULT_UNTARIFICATED_DEVICES,
    DOMAIN,
    HILO_ENERGY_TOTAL,
    LOG,
    MIN_SCAN_INTERVAL,
)

Ensuite, je changerais ceci dans ton code @RayLation

class HiloChallengeSensor(HiloEntity, RestoreEntity, SensorEntity):
    """Hilo challenge sensor.
    Its state will be either:
    - off: no ongoing or scheduled challenge
    - scheduled: A challenge is scheduled, details in the next_events
                 extra attribute
- pre_cold: optional phase to cool further before appreciation
    - appreciation: optional phase to pre-heat more before challenge             
    - pre_heat: Currently in the pre-heat phase
    - reduction or on: Challenge is currently active, heat is lowered
    - recovery: Challenge is completed, we're reheating.
    """

    def __init__(self, hilo, device, scan_interval):
        self._attr_name = "Defi Hilo"
        super().__init__(hilo, name=self._attr_name, device=device)
        self._attr_unique_id = slugify(self._attr_name)
        LOG.debug(f"Setting up ChallengeSensor entity: {self._attr_name}")
        self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL)
        self._state = "off"
        self._next_events = []
        self.async_update = Throttle(self.scan_interval)(self._async_update)

    @property
    def state(self):        
        if len(self._next_events) > 0:
            if datetime.now(timezone.utc) > self._next_events[0].phases.recovery_end:
                if len(self._next_events) > 1: 
                    # another challenge is scheduled after this one
                    return "scheduled"
                else:
                    return "off"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.recovery_start:
                return "recovery"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.reduction_start:
                return "reduction"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.preheat_start:
                return "pre_heat"
            elif datetime.now(timezone.utc) > self._next_events[0].phases.appreciation_start:
                return "appreciation"
        elif datetime.now(timezone.utc) > self._next_events[0].phases.appreciation_start - timedelta(hours = CONF_PRE_COLD):
            return "pre_cold"
            else:
                return "scheduled"
        else:
            return "off"

    @property
    def icon(self):
        if not self._device.available:
            return "mdi:lan-disconnect"
        if self.state == "appreciation":
            return "mdi:glass-cocktail"
        if self.state == "off":
            return "mdi:lightning-bolt"
        if self.state == "scheduled":
            return "mdi:progress-clock"
        if self.state == "pre_heat":
            return "mdi:radiator"
        if self.state in ["reduction", "on"]:
            return "mdi:power-plug-off"
        if self.state == "recovery":
            return "mdi:calendar-check"
        if self.state == "pre_cold":
            return "mdi:radiator-off"
        return "mdi:battery-alert"

    @property
    def should_poll(self):
        return True

    @property
    def extra_state_attributes(self):
        return {"next_events": self._next_events}

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()
        last_state = await self.async_get_last_state()
        if last_state:
            self._last_update = dt_util.utcnow()
            self._state = last_state.state
            self._next_events = last_state.attributes.get("next_events", [])

    async def _async_update(self):
        new_events = []
        events = await self._hilo._api.get_gd_events(self._hilo.devices.location_id)
        LOG.debug(f"Events received from Hilo: {events}")
        for raw_event in events:
            details = await self._hilo._api.get_gd_events(
                self._hilo.devices.location_id, event_id=raw_event["id"]
            )
            event = Event(**details)
            if self._hilo.appreciation > 0:
                event.appreciation(self._hilo.appreciation)
            new_events.append(event.as_dict())
        event = Event(**details)
        if self._hilo.pre_cold > 0:
            event.pre_cold(self._hilo.pre_cold)
        new_events.append(event.as_dict())        
    self._next_events = []
        if len(new_events):            
            self._next_events = new_events
            #we don't update the state here anymore, since it's now calculated in the "state"

@ic-dev21
Copy link
Collaborator

Et voilà:

image

Maintenant on attend le prochain défi... ou peut-être pas besoin ;)

@ic-dev21 ic-dev21 added enhancement New feature or request performance Performance labels Nov 30, 2023
@ic-dev21
Copy link
Collaborator

ic-dev21 commented Dec 2, 2023

#323 en test pour ça @arsenicks

@ic-dev21
Copy link
Collaborator

ic-dev21 commented Dec 8, 2023

Fixed dans #323 , #327 et dvd-dev/python-hilo#144

@ic-dev21 ic-dev21 closed this as completed Dec 8, 2023
@RayLation
Copy link

Nice, good job boys!

@arsenicks
Copy link
Collaborator Author

#323 en test pour ça @arsenicks

Fixed dans #323 , #327 et dvd-dev/python-hilo#144

Good job, sorry j'ai déconnecté un peu de mon setup pendant une semaine.. Merci pour ton aide et tes contribs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request performance Performance
Projects
None yet
Development

No branches or pull requests

5 participants