temperatura

temperatura

  • Monitorando a temperatura da sala dos servidores

    Por motivos que não cabem aqui, temos alguns servidores instalados em uma sala na empresa. E essa sala tem um ar-condicionado pra manter a temperatura sob controle.

    Um belo dia estou olhando os dados no Grafana e noto que essas máquinas não reportaram dados (não temos alertas enviados pelo Grafana, mas o porquê disso fica pra um outro dia). Entro na sala e a temperatura estava simplemente... 32°C. Era verão na Suécia, que é curto mas tem seus dias bem quentes. E o hardware das máquinas desligaram pra proteção.

    Entre entrar em contato com técnico do ar-condicionado e deixar a sala aberta pra ventilar, ficamos com aquele gosto amargo de não ter nenhum dado sobre a temperatura.

    A solução? raspberrypi!

    Ele tem um sensor que é vendido na Internet.

    O sensor já chegou mas não o raspberrypi. O motivo deve ser porque compramos um modelo que funciona como KVM e tem algumas coisas a mais.

    Então aqui a descrição de como botar o serviço pra funcionar em Linux.

    Dados via python

    Existe um software descrito na página do produto que aponta pro seguinte repositório no GitHub:

    Mas o repositório parece abandonado. Já faz 7 anos que ninguém manda nenhum commit. E o código não funciona com a versão mais moderna do sensor.

    O que fazer? Patch!

    Então corrigi o programa e criei um fork do repo original.

    Então temos o software pronto pra funcionar. Ou quase.

    Antes é preciso corrigir as permissões de leitura e escrita do dispositivo. E pra isso eu criei uma pequena regra no udev em /etc/udev/rules.d/90-temperature-sensor.rules

        
    KERNEL=="hidraw[0-9]*", SUBSYSTEM=="hidraw", SUBSYSTEMS=="usb", ATTRS{idVendor}=="3553", ATTRS{idProduct}=="a001", MODE="0666", SYMLINK+="temper"      
       
     

    Pegando a saída do kernel:

        
    # dmesg | grep -i temper
    [    5.152423] usb 1-10.4: Product: TEMPer2
    [    6.769788] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.0/0003:3553:A001.0005/input/input14
    [    6.826541] hid-generic 0003:3553:A001.0005: input,hidraw4: USB HID v1.11 Keyboard [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input0
    [    6.826720] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.1/0003:3553:A001.0006/input/input15
    [    6.827067] hid-generic 0003:3553:A001.0006: input,hidraw5: USB HID v1.10 Device [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input1
    [  939.507362] usb 1-10.4: Product: TEMPer2
    [  939.521617] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.0/0003:3553:A001.000B/input/input26
    [  939.580825] hid-generic 0003:3553:A001.000B: input,hidraw4: USB HID v1.11 Keyboard [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input0
    [  939.581808] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.1/0003:3553:A001.000C/input/input27
    [  939.582035] hid-generic 0003:3553:A001.000C: input,hidraw5: USB HID v1.10 Device [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input1
       
     

    É possível ver que o dispositivo aparece como dois devices: /dev/hidraw4 e /dev/hidraw5.

    Eu tentei usar a permissão 0644 primeiro, mas essa não funcionou pra ler os dados. Então tive de mudar pra 0666 mesmo sendo algo que só lê informação.

    Feita essa etapa, ainda não estamos prontos pra rodar o programa temper.py. Ainda é preciso instalar a dependência: serial.

    Se seu sistema é baseado em debian/ubuntu:

        
    > sudo apt install -y python3-serial
       
     

    Se não for, talvez seja mais fácil fazer com virtualenv. E pra isso eu atualmente uso o uv

       
    > uv venv venv
    > source venv/bin/activate     
    (venv)> uv pip install serial
      
    

    Tendo tudo pronto, chegamos ao momento da verdade:

       
    (venv)> ./temper.py 
    Bus 001 Dev 011 3553:a001 TEMPer2_V4.1 25.6C 78.0F - 22.8C 73.1F -
      
    

    Pegando a saída como JSON permite ver melhor o que é cada um desses resultados.

       
    (venv)>  ./temper.py --json
    [
        {
            "vendorid": 13651,
            "productid": 40961,
            "manufacturer": "PCsensor",
            "product": "TEMPer2",
            "busnum": 1,
            "devnum": 11,
            "devices": [
                "hidraw4",
                "hidraw5"
            ],
            "firmware": "TEMPer2_V4.1",
            "hex_firmware": "54454d506572325f56342e3100000000",
            "hex_data": "808009f64e200000800108e94e200000",
            "internal temperature": 25.5,
            "external temperature": 22.81
        }
    ]     
      
    

    Então a primeira temperatura lida, de /dev/hidraw4, é 25.5°C interna do dispositivo. A segunda, /dev/hidraw5, é de 22.81°C e externa, do cabo.

    Temos as leituras e os dados. Como mandar isso pro Grafana?

    Exportando as métricas pro Grafana

    Eu primeiramente tentei fazer em shell script e mandar o dados pro mimir, que é onde eu agrego as métricas.

    Fracassei miseravelmente.

    Não existe uma forma muito fácil de enviar um dados pro lá. O formato que o alloy usa é protobuf, que é um dado comprimido em snappy, etc.

    Qual outra alternativa?

    Expor o dado como open metric pro alloy pegar e enviar.

    Pode parecer simples mas... precisamos de um servidor web pra isso. Algo que temos fácil em python. Então usando uvicorn e fastapi podemos ter tudo funcionando. E é possível importar o temper.py como módulo.

    E é preciso incrementar nosso virtualenv (ou pacotes) com esses pacotes:

      
    > sudo apt install -y python3-uvicorn python3-fastapi
     
    
    ou
      
    (venv)> uv pip install uvicorn
    (venv)> uv pip install fastapi
     
    

    Sem mais delongas, eis aqui o código do monitor.py:

       
    #! /usr/bin/env python3
    
    import subprocess
    import argparse
    import logging
    import threading
    import time
    
    try:
        import temper
    except ImportError as e:
        print(f"Error importing temper module: {e}")
        print("Make sure python3-serial is installed: sudo apt-get install python3-serial")
        exit(1)
    
    import uvicorn
    from fastapi import FastAPI
    from fastapi.responses import PlainTextResponse
    
    
    CELSIUS = "\u2103"
    DEFAULT_PORT = 8000
    TEMPERATURE_MAX = 25.0
    
    logger = logging.getLogger(__file__)
    consoleOutputHandler = logging.StreamHandler()
    formatter = logging.Formatter(
        fmt="[%(asctime)s] (%(levelname)s) %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )
    consoleOutputHandler.setFormatter(formatter)
    logger.addHandler(consoleOutputHandler)
    logger.setLevel(logging.INFO)
    
    def shellExec(command: str) -> str:
        'run a command and return its output'
        try:
            return subprocess.getoutput(command)
        except Exception as e:
            logger.error(f"Error executing shell command '{command}': {e}")
            return f"Error: {e}"
    
    app = FastAPI()
    
    # Global temperature monitor instance (will be set in main)
    temperature_monitor = None
    
    @app.get("/metrics", response_class=PlainTextResponse)
    async def metrics():
        if temperature_monitor is None or temperature_monitor.temperature_current is None:
            return ""
            
        temperature_current = temperature_monitor.temperature_current
        logger.info(f"/metrics: {temperature_current}{CELSIUS}")
        data_lines = list()
        data_lines.append("#HELP server_room_temperature_celsius the room with servers current temperature")
        data_lines.append("#TYPE server_room_temperature_celsius gauge")
        data_lines.append(f"server_room_temperature_celsius {temperature_current}")
        data_lines.append("")
        return "\n".join(data_lines)
    
    class TemperatureMonitor:
        'A class that handle the temperature monitoring'
        port: int = DEFAULT_PORT
        temperature_max: float = TEMPERATURE_MAX
        temperature_current: float|None = None
        alert_lock: bool = False
    
        def __init__(self, port=None, temperature_max=None) -> None:
            if port:
                self.port = port
            if temperature_max:
                self.temperature_max = temperature_max
    
        def monitor(self) -> None:
            th = threading.Thread(target=self.webserver)
            th.daemon = True  # Make it a daemon thread
            th.start()
            try:
                while True:
                    self.update()
                    time.sleep(15)
            except KeyboardInterrupt:
                logger.info("Monitoring stopped by user")
            except Exception as e:
                logger.error(f"Error in monitoring loop: {e}")
                raise
         
        def webserver(self) -> None:
            uvicorn.run(app, host="127.0.0.1", port=self.port)
    
        def update(self) -> None:
            'Read the output from the command'
            try:
                tp = temper.Temper().read()
                if not tp or len(tp) == 0:
                    logger.warning("No temperature devices found")
                    self.temperature_current = None
                    return
                
                self.temperature_current = tp[0].get('external temperature')
                if self.temperature_current is not None:
                    logger.info(f"🌡️ current temperature: {self.temperature_current}{CELSIUS}")
                else:
                    logger.warning("External temperature reading is None")
            except Exception as e:
                logger.error(f"Error reading temperature sensor: {e}")
                self.temperature_current = None
    
    if __name__ == '__main__':
        parse = argparse.ArgumentParser(description="script to monitor temperature")
        parse.add_argument("--loglevel", default="info", help="the logging level (default=info)")
        parse.add_argument("--tempmax", type=float, default=TEMPERATURE_MAX, help="maximum temperature before raising alert")
        parse.add_argument("--port", type=int, default=DEFAULT_PORT, help="port to listen the service")
        args = parse.parse_args()
    
        # Validate log level
        valid_log_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
        if args.loglevel.upper() not in valid_log_levels:
            print(f"Invalid log level: {args.loglevel}. Valid options: {', '.join(valid_log_levels)}")
            exit(1)
            
        if args.loglevel.upper() != "INFO":
            logger.setLevel(args.loglevel.upper())
    
        # Validate temperature threshold
        if args.tempmax <= 0:
            print("Temperature threshold must be greater than 0")
            exit(1)
            
        # Validate port number
        if not (1 <= args.port <= 65535):
            print("Port must be between 1 and 65535")
            exit(1)
    
        # Create temperature monitor instance
        temperature_monitor = TemperatureMonitor(args.port, args.tempmax)
        temperature_monitor.monitor()     
      
    

    Eu tenho integrado uma parte de alerta que usa outro sistema, mas removi pra deixar o código fazendo somente o que é preciso.

    No alloy, adicionei as seguintes linhas:

      
    discovery.relabel "temperature_sensor" {
            targets = array.concat(
                    [{
                    __address__ = "localhost:8000",
                    }],
            )
    
            rule {
                    source_labels = ["__address__"]
                    target_label  = "instance"
                    replacement   = "temper"
            }
    }
    
    prometheus.scrape "temperature_sensor" {
            targets    = discovery.relabel.temperature_sensor.output
            forward_to = [prometheus.remote_write.prod.receiver]
            job_name   = "agent"
    }    
     
    

    Tudo pronto. Ou quase. Falta rodar o monitor.py que mostrei acima como serviço. E pra isso usamos o systemd. Basta criar o arquivo /etc/systemd/system/temperature-monitor.service e iniciar.

    Serviço no systemd

      
    [Unit]
    Description=Temperature monitoring service
    After=network.target
    
    [Service]
    User=helio
    Group=helio
    WorkingDirectory=/home/helio/temperature-sensor
    ExecStart=/home/helio/temperature-sensor/monitor.sh
    Restart=always
    
    [Install]
    WantedBy=multi-user.target    
     
    

    O script monitor.sh é pra somente ler o virtualenv corretamente:

      
    #! /usr/bin/env bash
    
    die() {
    	echo "ERROR: $@" >&2
    	echo "[$(date)] exiting with error"
    	exit 1
    }
    
    program="$0"
    root_dir=$(readlink -f $program)
    root_dir=$(dirname $root_dir)
    
    cd $root_dir
    
    source venv/bin/activate || \
        die "failed to read virtualenv"
    exec ./monitor.py    
     
    

    E iniciando o serviço:

      
    > sudo systemctl daemon-reload
    > sudo systemctl enable --now temperature-monitor    
     
    

    O que resta é criar um gráfico pra métrica server_room_temperature_celsius e partir pro abraço.

    Update: [Fri Sep 12 05:30:00 PM CEST 2025] acabo de perceber que o repositório que fiz fork é na verdade um fork de outro, que parece ser bem mais completo.

We use cookies

We use cookies on our website. Some of them are essential for the operation of the site, while others help us to improve this site and the user experience (tracking cookies). You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.