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

Warning nei log dopo l'aggiornamento 2024.5.0 di HA #39

Closed
dd310 opened this issue May 2, 2024 · 3 comments
Closed

Warning nei log dopo l'aggiornamento 2024.5.0 di HA #39

dd310 opened this issue May 2, 2024 · 3 comments
Assignees
Labels
bug Something isn't working

Comments

@dd310
Copy link

dd310 commented May 2, 2024

Dopo l'aggiornamento all'ultima versione di Home Assistant Core, il seguente warning compare nei log:

Registratore: homeassistant.util.loop
Fonte: util/loop.py:99
Prima occorrenza: 1 maggio 2024 alle ore 21:47:49 (1 occorrenze)
Ultima registrazione: 1 maggio 2024 alle ore 21:47:49

Detected blocking call to import_module inside the event loop by custom integration 'pun_sensor' at custom_components/pun_sensor/__init__.py, line 399: festivo = dataora in holidays.IT() (offender: /usr/local/lib/python3.12/site-packages/holidays/registry.py, line 224: self.entity = getattr(importlib.import_module(self.module_name), self.entity_name)), please create a bug report at https://github.com/virtualdj/pun_sensor/issues 
@riddik14
Copy link

riddik14 commented May 2, 2024

stesso probema: Detected blocking call to import_module inside the event loop by custom integration 'pun_sensor' at custom_components/pun_sensor/init.py, line 399: festivo = dataora in holidays.IT() (offender: /usr/local/lib/python3.12/site-packages/holidays/registry.py, line 224: self.entity = getattr(importlib.import_module(self.module_name), self.entity_name)), please create a bug report at https://github.com/virtualdj/pun_sensor/issues

@riddik14
Copy link

riddik14 commented May 2, 2024

########ho risolto cosi al file init.py

`"""Prezzi PUN del mese"""
from datetime import date, timedelta, datetime
from typing import Tuple
from bs4 import BeautifulSoup

from aiohttp import ClientSession
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.helpers.event import async_track_point_in_time, async_call_later
import homeassistant.util.dt as dt_util
from zoneinfo import ZoneInfo

from .const import (
DOMAIN,
PUN_FASCIA_MONO,
PUN_FASCIA_F23,
PUN_FASCIA_F1,
PUN_FASCIA_F2,
PUN_FASCIA_F3,
CONF_SCAN_HOUR,
CONF_ACTUAL_DATA_ONLY,
COORD_EVENT,
EVENT_UPDATE_FASCIA,
EVENT_UPDATE_PUN
)
import holidays
import logging

_LOGGER = logging.getLogger(name)

Usa sempre il fuso orario italiano (i dati del sito sono per il mercato italiano)

tz_pun = ZoneInfo('Europe/Rome')

Definisce i tipi di entità

PLATFORMS: list[str] = ["sensor"]

Carica le festività italiane

it_holidays = holidays.IT()

async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
"""Impostazione dell'integrazione da configurazione Home Assistant"""

# Salva il coordinator nella configurazione
coordinator = PUNDataUpdateCoordinator(hass, config)
hass.data.setdefault(DOMAIN, {})[config.entry_id] = coordinator

# Aggiorna immediatamente la fascia oraria corrente
await coordinator.update_fascia()

# Crea i sensori con la configurazione specificata
await hass.config_entries.async_forward_entry_setups(config, PLATFORMS)

# Schedula l'aggiornamento via web 10 secondi dopo l'avvio
coordinator.schedule_token = async_call_later(hass, timedelta(seconds=10), coordinator.update_pun)

# Registra il callback di modifica opzioni
config.async_on_unload(config.add_update_listener(update_listener))
return True

async def async_unload_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
"""Rimozione dell'integrazione da Home Assistant"""

# Scarica i sensori (disabilitando di conseguenza il coordinator)
unload_ok = await hass.config_entries.async_unload_platforms(config, PLATFORMS)
if unload_ok:
    hass.data[DOMAIN].pop(config.entry_id)

return unload_ok

async def update_listener(hass: HomeAssistant, config: ConfigEntry) -> None:
"""Modificate le opzioni da Home Assistant"""

# Recupera il coordinator
coordinator = hass.data[DOMAIN][config.entry_id]

# Aggiorna le impostazioni del coordinator dalle opzioni
if config.options[CONF_SCAN_HOUR] != coordinator.scan_hour:
    # Modificata l'ora di scansione
    coordinator.scan_hour = config.options[CONF_SCAN_HOUR]

    # Calcola la data della prossima esecuzione (all'ora definita)
    next_update_pun = dt_util.now().replace(hour=coordinator.scan_hour,
                            minute=0, second=0, microsecond=0)
    if next_update_pun.hour < dt_util.now().hour:
        # Se l'ora impostata è minore della corrente, schedula a domani
        # (perciò se è uguale esegue subito l'aggiornamento)
        next_update_pun = next_update_pun + timedelta(days=1)

    # Annulla eventuali schedulazioni attive
    if coordinator.schedule_token is not None:
        coordinator.schedule_token()
        coordinator.schedule_token = None

    # Schedula la prossima esecuzione
    coordinator.web_retries = 0
    coordinator.schedule_token = async_track_point_in_time(coordinator.hass, coordinator.update_pun, next_update_pun)
    _LOGGER.debug('Prossimo aggiornamento web: %s', next_update_pun.strftime('%d/%m/%Y %H:%M:%S %z'))

if config.options[CONF_ACTUAL_DATA_ONLY] != coordinator.actual_data_only:
    # Modificata impostazione 'Usa dati reali'
    coordinator.actual_data_only = config.options[CONF_ACTUAL_DATA_ONLY]
    _LOGGER.debug('Nuovo valore \'usa dati reali\': %s.', coordinator.actual_data_only)

    # Annulla eventuali schedulazioni attive
    if coordinator.schedule_token is not None:
        coordinator.schedule_token()
        coordinator.schedule_token = None

    # Esegue un nuovo aggiornamento immediatamente
    coordinator.web_retries = 0
    coordinator.schedule_token = async_call_later(coordinator.hass, timedelta(seconds=5), coordinator.update_pun)

class PUNDataUpdateCoordinator(DataUpdateCoordinator):
session: ClientSession

def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
    """Gestione dell'aggiornamento da Home Assistant"""
    super().__init__(
        hass,
        _LOGGER,
        # Nome dei dati (a fini di log)
        name = DOMAIN,
        # Nessun update_interval (aggiornamento automatico disattivato)
    )

    # Salva la sessione client e la configurazione
    self.session = async_get_clientsession(hass)

    # Inizializza i valori di configurazione (dalle opzioni o dalla configurazione iniziale)
    self.actual_data_only = config.options.get(CONF_ACTUAL_DATA_ONLY, config.data[CONF_ACTUAL_DATA_ONLY])
    self.scan_hour = config.options.get(CONF_SCAN_HOUR, config.data[CONF_SCAN_HOUR])

    # Inizializza i valori di default
    self.web_retries = 0
    self.schedule_token = None
    self.pun = [0.0, 0.0, 0.0, 0.0, 0.0]
    self.orari = [0, 0, 0, 0, 0]
    self.fascia_corrente = None
    _LOGGER.debug('Coordinator inizializzato (con \'usa dati reali\' = %s).', self.actual_data_only)

async def _async_update_data(self):
    """Aggiornamento dati a intervalli prestabiliti"""
    
    # Calcola l'intervallo di date per il mese corrente
    date_end = dt_util.now().date()
    date_start = date(date_end.year, date_end.month, 1)

    # All'inizio del mese, aggiunge i valori del mese precedente
    # a meno che CONF_ACTUAL_DATA_ONLY non sia impostato
    if (not self.actual_data_only) and (date_end.day < 4):
        date_start = date_start - timedelta(days=3)

    # URL del sito Mercato elettrico
    LOGIN_URL = 'https://www.mercatoelettrico.org/It/Tools/Accessodati.aspx?ReturnUrl=%2fIt%2fdownload%2fDownloadDati.aspx%3fval%3dMGP_Prezzi&val=MGP_Prezzi'
    DOWNLOAD_URL = 'https://www.mercatoelettrico.org/It/download/DownloadDati.aspx?val=MGP_Prezzi'
    
    # Apre la pagina per generare i cookie e i campi nascosti
    _LOGGER.debug('Connessione a URL login.')
    async with self.session.get(LOGIN_URL) as response:
        soup = BeautifulSoup(await response.read(), features='html.parser')
    
    # Recupera i campi nascosti __VIEWSTATE e __EVENTVALIDATION per la prossima richiesta
    viewstate = soup.find('input',{'name':'__VIEWSTATE'})['value']
    eventvalidation = soup.find('input',{'name':'__EVENTVALIDATION'})['value']
    login_payload = {
        'ctl00$ContentPlaceHolder1$CBAccetto1': 'on',
        'ctl00$ContentPlaceHolder1$CBAccetto2': 'on',
        'ctl00$ContentPlaceHolder1$Button1': 'Accetto',
        '__VIEWSTATE': viewstate,
        '__EVENTVALIDATION': eventvalidation
    }

    # Effettua il login (che se corretto porta alla pagina di download XML grazie al 'ReturnUrl')
    _LOGGER.debug('Invio credenziali a URL login.')
    async with self.session.post(LOGIN_URL, data=login_payload) as response:
        soup = BeautifulSoup(await response.read(), features='html.parser')

    # Recupera i campi nascosti __VIEWSTATE per la prossima richiesta
    viewstate = soup.find('input',{'name':'__VIEWSTATE'})['value']    
    data_request_payload = {
        'ctl00$ContentPlaceHolder1$tbDataStart': date_start.strftime('%d/%m/%Y'),
        'ctl00$ContentPlaceHolder1$tbDataStop': date_end.strftime('%d/%m/%Y'),
        'ctl00$ContentPlaceHolder1$btnScarica': 'scarica+file+xml+compresso',
        '__VIEWSTATE': viewstate
    }

    # Effettua il download dello ZIP con i file XML
    _LOGGER.debug('Inizio download file ZIP con XML.')
    async with self.session.post(DOWNLOAD_URL, data=data_request_payload) as response:
        # Scompatta lo ZIP in memoria
        try:
            archive = zipfile.ZipFile(io.BytesIO(await response.read()))
        except:
            # Esce perché l'output non è uno ZIP
            raise UpdateFailed('Archivio ZIP scaricato dal sito non valido.')

    # Mostra i file nell'archivio
    _LOGGER.debug(f'{ len(archive.namelist()) } file trovati nell\'archivio (' + ', '.join(str(fn) for fn in archive.namelist()) + ').')

    # Carica le festività
    it_holidays = holidays.IT()

    # Inizializza le variabili di conteggio dei risultati
    mono = []
    f1 = []
    f2 = []
    f3 = []

    # Esamina ogni file XML nello ZIP (ordinandoli prima)
    for fn in sorted(archive.namelist()):
        # Scompatta il file XML in memoria
        xml_tree = et.parse(archive.open(fn))

        # Parsing dell'XML (1 file = 1 giorno)
        xml_root = xml_tree.getroot()

        # Estrae la data dal primo elemento (sarà identica per gli altri)
        dat_string = xml_root.find('Prezzi').find('Data').text #YYYYMMDD

        # Converte la stringa giorno in data
        dat_date = date(int(dat_string[0:4]), int(dat_string[4:6]), int(dat_string[6:8]))

        # Verifica la festività
        festivo = dat_date in it_holidays

        # Estrae le rimanenti informazioni
        for prezzi in xml_root.iter('Prezzi'):
            # Estrae l'ora dall'XML
            ora = int(prezzi.find('Ora').text) - 1 # 1..24
            
            # Estrae il prezzo PUN dall'XML in un float
            prezzo_string = prezzi.find('PUN').text
            prezzo_string = prezzo_string.replace('.','').replace(',','.')
            prezzo = float(prezzo_string) / 1000

            # Estrae la fascia oraria
            fascia = get_fascia_for_xml(dat_date, festivo, ora)

            # Calcola le statistiche
            mono.append(prezzo)
            if fascia == 3:
                f3.append(prezzo)
            elif fascia == 2:
                f2.append(prezzo)
            elif fascia == 1:
                f1.append(prezzo)

    # Salva i risultati nel coordinator
    self.orari[PUN_FASCIA_MONO] = len(mono)
    self.orari[PUN_FASCIA_F1] = len(f1)
    self.orari[PUN_FASCIA_F2] = len(f2)
    self.orari[PUN_FASCIA_F3] = len(f3)
    if self.orari[PUN_FASCIA_MONO] > 0:
        self.pun[PUN_FASCIA_MONO] = mean(mono)
    if self.orari[PUN_FASCIA_F1] > 0:
        self.pun[PUN_FASCIA_F1] = mean(f1)
    if self.orari[PUN_FASCIA_F2] > 0:
        self.pun[PUN_FASCIA_F2] = mean(f2)
    if self.orari[PUN_FASCIA_F3] > 0:
        self.pun[PUN_FASCIA_F3] = mean(f3)

    # Calcola la fascia F23 (a partire da F2 ed F3)
    # NOTA: la motivazione del calcolo è oscura ma sembra corretta; vedere:
    # https://github.com/virtualdj/pun_sensor/issues/24#issuecomment-1829846806
    if self.orari[PUN_FASCIA_F2] > 0 and self.orari[PUN_FASCIA_F3] > 0:
        # Esistono dati sia per F2 che per F3
        self.orari[PUN_FASCIA_F23] = self.orari[PUN_FASCIA_F2] + self.orari[PUN_FASCIA_F3]
        self.pun[PUN_FASCIA_F23] = 0.46 * self.pun[PUN_FASCIA_F2] + 0.54 * self.pun[PUN_FASCIA_F3]
    else:
        # Devono esserci dati sia per F2 che per F3 affinché il risultato sia valido
        self.orari[PUN_FASCIA_F23] = 0
        self.pun[PUN_FASCIA_F23] = 0
   
    # Logga i dati
    _LOGGER.debug('Numero di dati: ' + ', '.join(str(i) for i in self.orari))
    _LOGGER.debug('Valori PUN: ' + ', '.join(str(f) for f in self.pun))
    return

async def update_fascia(self, now=None):
    """Aggiorna la fascia oraria corrente"""

    # Scrive l'ora corrente (a scopi di debug)
    _LOGGER.debug('Ora corrente sistema: %s', dt_util.now().strftime('%a %d/%m/%Y %H:%M:%S %z'))
    _LOGGER.debug('Ora corrente fuso orario italiano: %s', dt_util.now(time_zone=tz_pun).strftime('%a %d/%m/%Y %H:%M:%S %z'))

    # Ottiene la fascia oraria corrente e il prossimo aggiornamento
    self.fascia_corrente, next_update_fascia = get_fascia(dt_util.now(time_zone=tz_pun))
    _LOGGER.info('Nuova fascia corrente: F%s (prossima: %s)', self.fascia_corrente, next_update_fascia.strftime('%a %d/%m/%Y %H:%M:%S %z'))

    # Notifica che i dati sono stati aggiornati (fascia)
    self.async_set_updated_data({ COORD_EVENT: EVENT_UPDATE_FASCIA })

    # Schedula la prossima esecuzione
    async_track_point_in_time(self.hass, self.update_fascia, next_update_fascia)

async def update_pun(self, now=None):
    """Aggiorna i prezzi PUN da Internet (funziona solo se schedulata)"""

    # Aggiorna i dati da web
    try:
        # Esegue l'aggiornamento
        await self._async_update_data()

        # Se non ci sono eccezioni, ha avuto successo
        self.web_retries = 0
    except Exception as e:
        # Errori durante l'esecuzione dell'aggiornamento, riprova dopo
        if (self.web_retries == 0):
            # Primo errore, riprova dopo 1 minuto
            self.web_retries = 5
            retry_in_minutes = 1
        elif (self.web_retries == 5):
            # Secondo errore, riprova dopo 10 minuti
            self.web_retries -= 1
            retry_in_minutes = 10
        elif (self.web_retries == 1):
            # Ultimo errore, tentativi esauriti
            self.web_retries = 0

            # Schedula al giorno dopo
            retry_in_minutes = 0
        else:
            # Ulteriori errori (4, 3, 2)
            self.web_retries -= 1
            retry_in_minutes = 60 * (4 - self.web_retries)

        # Annulla eventuali schedulazioni attive
        if self.schedule_token is not None:
            self.schedule_token()
            self.schedule_token = None

        # Prepara la schedulazione
        if (retry_in_minutes > 0):
            # Minuti dopo
            _LOGGER.warn('Errore durante l\'aggiornamento via web, nuovo tentativo tra %s minut%s.', retry_in_minutes, 'o' if retry_in_minutes == 1 else 'i', exc_info=e)
            self.schedule_token = async_call_later(self.hass, timedelta(minutes=retry_in_minutes), self.update_pun)
        else:
            # Giorno dopo
            _LOGGER.error('Errore durante l\'aggiornamento via web, tentativi esauriti.', exc_info=e)
            next_update_pun = dt_util.now().replace(hour=self.scan_hour,
                            minute=0, second=0, microsecond=0) + timedelta(days=1)
            self.schedule_token = async_track_point_in_time(self.hass, self.update_pun, next_update_pun)
            _LOGGER.debug('Prossimo aggiornamento web: %s', next_update_pun.strftime('%d/%m/%Y %H:%M:%S %z'))
        
        # Esce e attende la prossima schedulazione
        return

    # Notifica che i dati PUN sono stati aggiornati con successo
    self.async_set_updated_data({ COORD_EVENT: EVENT_UPDATE_PUN })

    # Calcola la data della prossima esecuzione
    next_update_pun = dt_util.now().replace(hour=self.scan_hour,
                            minute=0, second=0, microsecond=0)
    if next_update_pun <= dt_util.now():
        # Se l'evento è già trascorso la esegue domani alla stessa ora
        next_update_pun = next_update_pun + timedelta(days=1)

    # Annulla eventuali schedulazioni attive
    if self.schedule_token is not None:
        self.schedule_token()
        self.schedule_token = None

    # Schedula la prossima esecuzione
    self.schedule_token = async_track_point_in_time(self.hass, self.update_pun, next_update_pun)
    _LOGGER.debug('Prossimo aggiornamento web: %s', next_update_pun.strftime('%d/%m/%Y %H:%M:%S %z'))

def get_fascia_for_xml(data, festivo, ora) -> int:
"""Restituisce il numero di fascia oraria di un determinato giorno/ora"""
#F1 = lu-ve 8-19
#F2 = lu-ve 7-8, lu-ve 19-23, sa 7-23
#F3 = lu-sa 0-7, lu-sa 23-24, do, festivi
if festivo or (data.weekday() == 6):
# Festivi e domeniche
return 3
elif (data.weekday() == 5):
# Sabato
if (ora >= 7) and (ora < 23):
return 2
else:
return 3
else:
# Altri giorni della settimana
if (ora == 7) or ((ora >= 19) and (ora < 23)):
return 2
elif (ora == 23) or ((ora >= 0) and (ora < 7)):
return 3
return 1

def get_fascia(dataora: datetime) -> Tuple[int, datetime]:
"""Restituisce la fascia della data/ora indicata (o quella corrente) e la data del prossimo cambiamento"""

# Verifica se la data corrente è un giorno con festività
festivo = dataora in holidays.IT()

# Identifica la fascia corrente
# F1 = lu-ve 8-19
# F2 = lu-ve 7-8, lu-ve 19-23, sa 7-23
# F3 = lu-sa 0-7, lu-sa 23-24, do, festivi
if festivo or (dataora.weekday() == 6):
    # Festivi e domeniche
    fascia = 3

    # Prossima fascia: alle 7 di un giorno non domenica o festività
    prossima = (dataora + timedelta(days=1)).replace(hour=7,
                    minute=0, second=0, microsecond=0)
    while ((prossima in holidays.IT()) or (prossima.weekday() == 6)):
        prossima += timedelta(days=1)

elif (dataora.weekday() == 5):
    # Sabato
    if (dataora.hour >= 7) and (dataora.hour < 23):
        # Sabato dalle 7 alle 23
        fascia = 2

        # Prossima fascia: alle 23 dello stesso giorno
        prossima = dataora.replace(hour=23,
                        minute=0, second=0, microsecond=0)
    elif (dataora.hour < 7):
        # Sabato tra le 0 e le 7
        fascia = 3

        # Prossima fascia: alle 7 dello stesso giorno
        prossima = dataora.replace(hour=7,
                        minute=0, second=0, microsecond=0)
    else:
        # Sabato dopo le 23
        fascia = 3

        # Prossima fascia: alle 7 di un giorno non domenica o festività
        prossima = (dataora + timedelta(days=1)).replace(hour=7,
                    minute=0, second=0, microsecond=0)
        while ((prossima in holidays.IT()) or (prossima.weekday() == 6)):
            prossima += timedelta(days=1)
else:
    # Altri giorni della settimana
    if (dataora.hour == 7):
        # Lunedì-venerdì dalle 7 alle 8
        fascia = 2

        # Prossima fascia: alle 8 dello stesso giorno
        prossima = dataora.replace(hour=8,
                        minute=0, second=0, microsecond=0)

    elif ((dataora.hour >= 19) and (dataora.hour < 23)):
        # Lunedì-venerdì dalle 19 alle 23
        fascia = 2

        # Prossima fascia: alle 23 dello stesso giorno
        prossima = dataora.replace(hour=23,
                        minute=0, second=0, microsecond=0)

    elif ((dataora.hour == 23) or ((dataora.hour >= 0) and (dataora.hour < 7))):
        # Lunedì-venerdì dalle 23 alle 24 e dalle 0 alle 7
        fascia = 3

        # Prossima fascia: alle 7 di un giorno non domenica o festività
        prossima = dataora.replace(hour=7,
                    minute=0, second=0, microsecond=0)
        while ((prossima <= dataora) or (prossima in holidays.IT()) or (prossima.weekday() == 6)):
            prossima += timedelta(days=1)

    else:
        # Lunedì-venerdì dalle 8 alle 19
        fascia = 1

        # Prossima fascia: alle 19 dello stesso giorno
        prossima = dataora.replace(hour=19,
                        minute=0, second=0, microsecond=0)

return fascia, prossima

`

@virtualdj
Copy link
Owner

virtualdj commented May 2, 2024

Non sembra sia sufficiente (@riddik14 per cortesia edita e sistema tutto tra tag codice sennò non si capisce nulla), perché questo sistema solo il calcolo delle fasce orarie, ma quando si scaricano i prezzi c'è pure questo errore:

[homeassistant.util.loop] Detected blocking call to import_module inside the event loop by custom integration 'pun_sensor' at custom_components/pun_sensor/__init__.py, line 172: soup = BeautifulSoup(await response.read(), features='html.parser') (offender: /srv/homeassistant/lib/python3.12/site-packages/charset_normalizer/utils.py, line 256: importlib.import_module("encodings.{}".format(name)).IncrementalDecoder,), please create a bug report at https://github.com/virtualdj/pun_sensor/issues

Ovviamente mica è facile capire come sbrogliare la matassa... mettono i paletti con le nuove versioni di HA, con 2 righe di informazioni sul blog per sviluppatori (se va bene, vedi questo per esempio) senza spiegare nel dettaglio alcunché.

Sicuramente è colpa mia che non sono così skillato in Python, però cavolo sembra di avere a che fare con la documentazione di Google!

@virtualdj virtualdj self-assigned this May 2, 2024
@virtualdj virtualdj added bug Something isn't working help wanted Extra attention is needed and removed help wanted Extra attention is needed labels May 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants