import requests # type: ignore import pandas as pd # type: ignore from typing import Optional, Dict, List class PIMSClient: """ Cliente para interagir com a API do PIMS (PI System) da Votorantim. Esta classe fornece métodos para autenticação e busca de dados de streams/tags do sistema PIMS através da API REST. """ def __init__(self, base_url: str, api_key: Optional[str] = None, api_key_header: Optional[str] = "apikey", additional_headers: Optional[Dict[str, str]] = None): """ Inicializa o cliente PIMS. Args: base_url (str): URL base da API (ex: https://votorantim.apimanagement.br10.hana.ondemand.com/v2/webapi/piwebapi) api_key (Optional[str]): API Key para autenticação api_key_header (Optional[str]): Nome do header onde a chave deve ser enviada (ex: "X-API-Key", "Ocp-Apim-Subscription-Key", "apikey") additional_headers (Optional[Dict[str, str]]): Cabeçalhos adicionais para incluir em todas as requisições """ self.base_url = base_url.rstrip('/') self.api_key = api_key self.api_key_header = api_key_header self.session = requests.Session() self.additional_headers = additional_headers or {} self._authenticated = False def authenticate(self) -> bool: """ Configura a autenticação via cabeçalhos. Returns: bool: True se a configuração foi bem-sucedida, False caso contrário """ try: default_headers: Dict[str, str] = { 'Content-Type': 'application/json', 'Accept': 'application/json' } if self.api_key and self.api_key_header: default_headers[self.api_key_header] = self.api_key # Mescla cabeçalhos adicionais (sobrescrevem os padrões se necessário) default_headers.update(self.additional_headers) self.session.headers.update(default_headers) # Não faz chamada de teste aqui para evitar 401 em endpoints protegidos; assume headers configurados self._authenticated = True return True except requests.exceptions.RequestException as e: print(f"Erro na configuração da autenticação: {e}") return False def get_stream_data(self, web_ids: Dict[str, str], start_time: str = "*-3d", end_time: str = "*") -> Optional[pd.DataFrame]: """ Busca dados de múltiplos streams/tags. Args: web_ids (Dict[str, str]): Dicionário no formato {tag_name: web_id} start_time (str): Data/hora de início (formato: "*-3d" ou "yyyy-mm-dd") end_time (str): Data/hora de fim (formato: "*" ou "yyyy-mm-dd") Returns: pd.DataFrame: DataFrame onde as colunas são o nome da tag, o índice é o Timestamp, e os valores são os valores das tags """ if not self._authenticated: if not self.authenticate(): print("Falha na autenticação") return None all_series = [] for tag_name, web_id in web_ids.items(): try: url = f"{self.base_url}/streams/{web_id}/recorded" params = { "startTime": start_time, "endTime": end_time } response = self.session.get(url, params=params) response.raise_for_status() data = response.json() if 'Items' in data and data['Items']: df = pd.DataFrame(data['Items']) if 'Timestamp' in df.columns and 'Value' in df.columns: # Converte timestamp try: df['Timestamp'] = pd.to_datetime( df['Timestamp'], format='ISO8601', utc=True, errors='coerce' ) except TypeError: df['Timestamp'] = pd.to_datetime( df['Timestamp'], utc=True, errors='coerce' ) # Arredonda timestamps para precisão de segundos df['Timestamp'] = df['Timestamp'].dt.floor('s') # Normaliza valores: quando a API retorna um dict, tenta extrair um número def _extract_numeric(v): if isinstance(v, dict): # Casos comuns: {'Value': } ou aninhados inner = v.get('Value') if isinstance(inner, (int, float)): return inner # Tenta outros campos conhecidos for k in ('NumericValue',): inner2 = v.get(k) if isinstance(inner2, (int, float)): return inner2 return None if isinstance(v, (int, float)): return v # Converte strings numéricas, demais viram NaN return pd.to_numeric(v, errors='coerce') df['Value'] = df['Value'].apply(_extract_numeric) df['Value'] = pd.to_numeric(df['Value'], errors='coerce') df.set_index('Timestamp', inplace=True) # Agrega valores por segundo para remover índices duplicados series = ( df['Value'] .groupby(level=0) .mean() .sort_index() .rename(tag_name) ) all_series.append(series) else: print(f"Nenhum dado encontrado para a tag '{tag_name}' no período especificado") except requests.exceptions.RequestException as e: print(f"Erro ao buscar dados do stream {tag_name}: {e}") if all_series: result_df = pd.concat(all_series, axis=1) return result_df else: print("Nenhum dado encontrado para as tags informadas.") return pd.DataFrame() def search_streams(self, name_filter: Optional[str] = None, tag_filter: Optional[str] = None) -> Optional[List[Dict]]: """ Busca streams disponíveis com filtros opcionais. Args: name_filter (str): Filtro por nome do stream tag_filter (str): Filtro por tag Returns: List[Dict]: Lista de streams encontrados ou None se houver erro """ if not self._authenticated: if not self.authenticate(): print("Falha na autenticação") return None try: search_url = f"{self.base_url}/streams" params = {} if name_filter: params['nameFilter'] = name_filter if tag_filter: params['tag'] = tag_filter response = self.session.get(search_url, params=params) response.raise_for_status() return response.json().get('Items', []) except requests.exceptions.RequestException as e: print(f"Erro ao buscar streams: {e}") return None def get_stream_info(self, web_id: str) -> Optional[Dict]: """ Obtém informações detalhadas de um stream específico. Args: web_id (str): WebID do stream Returns: Dict: Informações do stream ou None se houver erro """ if not self._authenticated: if not self.authenticate(): print("Falha na autenticação") return None try: info_url = f"{self.base_url}/streams/{web_id}" response = self.session.get(info_url) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Erro ao buscar informações do stream: {e}") return None def get_web_ids_by_tags(self, data_server_id: str, tag_names: List[str]) -> Dict[str, Optional[str]]: """ Retorna os WebIds para uma lista de tags (pontos) em um Data Server específico. Args: data_server_id (str): ID/WebId do Data Server (ex: "F1DS-...") tag_names (List[str]): Lista com os nomes exatos das tags Returns: Dict[str, Optional[str]]: Dicionário mapeando tag -> WebId (ou None se não encontrada) """ # Garante autenticação, similar ao script de teste if not self._authenticated: if not self.authenticate(): print("Falha na autenticação") return {tag: None for tag in tag_names} results: Dict[str, Optional[str]] = {} for tag in tag_names: try: # Monta a URL seguindo a lógica do script de teste fornecido no contexto url = f"{self.base_url}/dataservers/{data_server_id}/points" params = {"namefilter": tag} print(url, params) response = self.session.get(url, params=params) response.raise_for_status() data = response.json() # Corrige: procurar a lista 'Items' como no script de teste items = data.get("Items", []) if isinstance(data, dict) else [] web_id_value: Optional[str] = None if items: # Emula exatamente o resultado do script: pega primeiro item se disponível first_item = items[0] if isinstance(first_item, dict): web_id_value = first_item.get("WebId") results[tag] = web_id_value except requests.exceptions.RequestException as e: print(f"Erro ao buscar WebId para tag '{tag}': {e}") results[tag] = None return results def multi_tags_agregadas( self, web_ids: List[str], start_time: str, end_time: str, summary_duration: str = "15m", summary_type: str = "average", selected_fields: str = "Items.Name;Items.Items.Type;Items.Items.Value.Timestamp;Items.Items.Value.Value;Items.Items.Value.Good", batch_size: int = 50, ) -> Optional[Dict]: """ Chama o endpoint /streamsets/summary com múltiplos webids via GET e retorna o JSON bruto. Para evitar URLs muito longas, realiza chamadas em lotes e agrega os resultados. Args: web_ids (List[str]): Lista de WebIds a consultar start_time (str): Início do período (ex.: "2024-09-05" ou "*-1d") end_time (str): Fim do período (ex.: "2024-09-06" ou "*") summary_duration (str): Duração do resumo (ex.: "15m") summary_type (str): Tipo de resumo (ex.: "average", "minimum", "maximum", etc.) selected_fields (str): Campos a retornar batch_size (int): Tamanho do lote de WebIds por requisição Returns: Optional[Dict]: JSON com "Items" unificados ou None em caso de erro """ if not self._authenticated: if not self.authenticate(): print("Falha na autenticação") return None try: url = f"{self.base_url}/streamsets/summary" all_items: List[Dict] = [] for i in range(0, len(web_ids), batch_size): chunk = web_ids[i:i + batch_size] # Constrói lista de tuplas para repetir 'webid' como múltiplos params params: List[tuple] = [("webid", wid) for wid in chunk] params.extend([ ("startTime", start_time), ("endtime", end_time), # conforme imagem ("summaryDuration", summary_duration), ("summaryType", summary_type), ("selectedFields", selected_fields), ]) response = self.session.get(url, params=params, timeout=600000) response.raise_for_status() data = response.json() items = data.get("Items", []) if isinstance(data, dict) else [] if isinstance(items, list): all_items.extend(items) return {"Items": all_items} except requests.exceptions.RequestException as e: print(f"Erro ao buscar dados brutos de múltiplas tags: {e}") return None def multi_tags_agregadas_df( self, web_ids: List[str], start_time: str, end_time: str, summary_duration: str = "1m", summary_type: str = "average", selected_fields: str = "Items.Name;Items.Items.Type;Items.Items.Value.Timestamp;Items.Items.Value.Value;Items.Items.Value.Good" ) -> pd.DataFrame: """ Chama /streamsets/summary para múltiplos webids e retorna DataFrame: - índice: Timestamp (precisão de segundos) - colunas: nome da tag - células: Value (numérico) """ raw = self.multi_tags_agregadas( web_ids=web_ids, start_time=start_time, end_time=end_time, summary_duration=summary_duration, summary_type=summary_type, selected_fields=selected_fields, ) if not raw or 'Items' not in raw or not isinstance(raw['Items'], list): return pd.DataFrame() records = [] def _extract_numeric(v): if isinstance(v, dict): inner = v.get('Value') if isinstance(inner, (int, float)): return inner for k in ('NumericValue',): inner2 = v.get(k) if isinstance(inner2, (int, float)): return inner2 return None if isinstance(v, (int, float)): return v return pd.to_numeric(v, errors='coerce') for entry in raw['Items']: tag_name = entry.get('Name') series_items = entry.get('Items') or [] for it in series_items: v = (it.get('Value') or {}) if isinstance(it, dict) else {} ts = v.get('Timestamp') if isinstance(v, dict) else None val = v.get('Value') if isinstance(v, dict) else None val = _extract_numeric(val) if ts is not None: records.append({ 'Timestamp': ts, 'Tag': tag_name, 'Value': val, }) if not records: return pd.DataFrame() df = pd.DataFrame.from_records(records) # Converte e arredonda timestamps try: df['Timestamp'] = pd.to_datetime(df['Timestamp'], format='ISO8601', utc=True, errors='coerce') except TypeError: df['Timestamp'] = pd.to_datetime(df['Timestamp'], utc=True, errors='coerce') df['Timestamp'] = df['Timestamp'].dt.floor('s') # Pivot: índice timestamp, colunas nome da tag, valores numéricos df_pivot = df.pivot_table(index='Timestamp', columns='Tag', values='Value', aggfunc='mean') df_pivot.sort_index(inplace=True) return df_pivot def multi_tags_brutas( self, web_ids: List[str], start_time: str, end_time: str, max_count: int = 10000, batch_size: int = 50, ) -> Optional[Dict]: """ Chama o endpoint /streamsets/recorded com múltiplos webids via GET e retorna o JSON bruto. Para evitar URLs muito longas, realiza chamadas em lotes e agrega os resultados. Args: web_ids (List[str]): Lista de WebIds a consultar start_time (str): Início do período (ex.: "2024-09-01" ou "*-1d") end_time (str): Fim do período (ex.: "2024-09-09" ou "*") max_count (int): Número máximo de registros a retornar por requisição batch_size (int): Tamanho do lote de WebIds por requisição Returns: Optional[Dict]: JSON com "Items" unificados ou None em caso de erro """ if not self._authenticated: if not self.authenticate(): print("Falha na autenticação") return None try: url = f"{self.base_url}/streamsets/recorded" all_items: List[Dict] = [] for i in range(0, len(web_ids), batch_size): chunk = web_ids[i:i + batch_size] # Constrói lista de tuplas para repetir 'webid' como múltiplos params params: List[tuple] = [("webid", wid) for wid in chunk] params.extend([ ("startTime", start_time), ("endTime", end_time), ("maxCount", str(max_count)), ]) response = self.session.get(url, params=params, timeout=600000) response.raise_for_status() data = response.json() items = data.get("Items", []) if isinstance(data, dict) else [] if isinstance(items, list): all_items.extend(items) return {"Items": all_items} except requests.exceptions.RequestException as e: print(f"Erro ao buscar dados brutos de múltiplas tags: {e}") return None def multi_tags_brutas_df( self, web_ids: List[str], start_time: str, end_time: str, max_count: int = 10000, ) -> pd.DataFrame: """ Chama /streamsets/recorded para múltiplos webids e retorna DataFrame: - índice: Timestamp (precisão de segundos) - colunas: nome da tag - células: Value (numérico) Args: web_ids (List[str]): Lista de WebIds a consultar start_time (str): Início do período (ex.: "2024-09-01" ou "*-1d") end_time (str): Fim do período (ex.: "2024-09-09" ou "*") max_count (int): Número máximo de registros a retornar por requisição Returns: pd.DataFrame: DataFrame com timestamp como índice e tags como colunas """ raw = self.multi_tags_brutas( web_ids=web_ids, start_time=start_time, end_time=end_time, max_count=max_count, ) if not raw or 'Items' not in raw or not isinstance(raw['Items'], list): return pd.DataFrame() records = [] def _extract_numeric(v): if isinstance(v, dict): inner = v.get('Value') if isinstance(inner, (int, float)): return inner for k in ('NumericValue',): inner2 = v.get(k) if isinstance(inner2, (int, float)): return inner2 return None if isinstance(v, (int, float)): return v return pd.to_numeric(v, errors='coerce') for entry in raw['Items']: tag_name = entry.get('Name') # Para /streamsets/recorded, cada entry tem uma lista 'Items' com objetos contendo Timestamp e Value diretamente series_items = entry.get('Items') or [] # Processa items aninhados for it in series_items: if isinstance(it, dict): # Estrutura: {'Timestamp': '2024-09-01T23:00:00Z', 'Value': 4431.94141, ...} if 'Timestamp' in it and 'Value' in it: ts = it.get('Timestamp') val = it.get('Value') val = _extract_numeric(val) if ts is not None: records.append({ 'Timestamp': ts, 'Tag': tag_name, 'Value': val, }) if not records: return pd.DataFrame() df = pd.DataFrame.from_records(records) # Converte e arredonda timestamps try: df['Timestamp'] = pd.to_datetime(df['Timestamp'], format='ISO8601', utc=True, errors='coerce') except TypeError: df['Timestamp'] = pd.to_datetime(df['Timestamp'], utc=True, errors='coerce') df['Timestamp'] = df['Timestamp'].dt.floor('s') # Pivot: índice timestamp, colunas nome da tag, valores numéricos # Agrega valores duplicados no mesmo timestamp usando média df_pivot = df.pivot_table(index='Timestamp', columns='Tag', values='Value', aggfunc='mean') df_pivot.sort_index(inplace=True) return df_pivot def close(self): """Fecha a sessão HTTP.""" self.session.close() # Exemplo de uso if __name__ == "__main__": # Configuração do cliente base_url = "https://votorantim.apimanagement.br10.hana.ondemand.com/v2/webapi/piwebapi" api_key = "zK4WbZAZGBwSaQ5GJzhPpp06P1PGueqP" # Cria instância do cliente pims_client = PIMSClient(base_url, api_key) # Exemplo: buscar dados de um stream específico tag_forms = [ 'CI-J3J01S1', 'CI-J3P01T1A', 'CI-J3P03S1', 'CI-W3A05F1', 'CI-W3A50A1', 'CI-W3A50A2', 'CI-W3A50A3', 'CI-W3A50P1', 'CI-W3A50T1', 'CI-W3A55P1', 'CI-W3A55T1', 'CI-W3A65_Cl', 'CI-W3A65_SO3', 'CI-W3A71P1', 'CI-W3A71P2', 'CI-W3A71P3', 'CI-W3E01F1', 'CI-W3K01S1', 'CI-W3K01T1', 'CI-W3K01T2', 'CI-W3K01T3', 'CI-W3K01T4', 'CI-W3K14P1', 'CI-W3P17S1', 'CI-W3V04P1', 'CI-W3V04P3', 'CI-W3V21F1', 'CI-W3V21P1', 'CI-W3V30F1', 'CI-W3V33P1', 'CI-W3W01A1', 'CI-W3W01A2', 'CI-W3W01A3', 'CI-W3W01G1', 'CI-W3W01P1', 'CI-W3W01P2', 'CI-W3W03I1', 'CI-W3W03S1', 'CI-W3X21IN', 'CI-W3_C3S', 'CI-W3_CAO', 'CI-W3_MA', 'CI-W3_MS', 'CI-W3_PL' ] web_ids = pims_client.get_web_ids_by_tags("F1DS-7fYgsRTtUOa7V9NIwSujAUElIQVZD", tag_forms) web_ids_list: List[str] = [wid for wid in web_ids.values() if isinstance(wid, str)] from datetime import datetime, timedelta # Parâmetros iniciais apenas até o dia (granulometria diária) inicio = datetime(2023, 1, 1) # Somente a data, sem horas/minutos/segundos fim = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # Até hoje à 00:00 (começo do dia atual) delta = timedelta(days=1) dfs = [] # lista para armazenar os dataframes parciais while inicio < fim: proximo = min(inicio + delta, fim) # garante que não passa da data atual print(f"Buscando de {inicio:%Y-%m-%d} até {proximo:%Y-%m-%d}...") df_parcial = pims_client.multi_tags_brutas_df( web_ids_list, inicio.strftime("%Y-%m-%d"), proximo.strftime("%Y-%m-%d"), max_count=1000 ) dfs.append(df_parcial) inicio = proximo # avança o cursor # break # concatena todos em um único dataframe df_final = pd.concat(dfs, ignore_index=False) df_final.reset_index(inplace=True) df_final.rename(columns={'index': 'timestamp'}, inplace=True) # df_final = df_final.ffill() # df_final = df_final.bfill() # save to csv if not df_final.empty: df_final.to_parquet("data_brutos_pims_no_fill.parquet", index=False) print(df_final.head()) print(df_final.shape) print(df_final.columns)