Pular para o conteúdo
Node.js

Node.js: alterando o plano de energia do Windows

Paloma Macetko
Ilustração colorida de um unicórnio de crina arco-íris girando um grande seletor mágico de três posições, cada uma ligada a um cristal de energia com brilho diferente, ao lado de uma coruja escriba conferindo três selos e um castelo com velas flutuantes ao fundo

Olá meus Unicórnios! 🦄✨

Trocar o plano de energia do Windows por código parece a tarefa mais boba do mundo. 😅 Tem um comando pronto que vem com o sistema, ele aceita um argumento, e acabou. É aquele tipo de coisa que você resolve em cinco minutos entre duas tarefas de verdade.

Foi exatamente o que eu pensei. E aí eu fui rodar, conferir a saída, e descobri que duas das minhas suposições estavam erradas — uma delas escrita com todas as letras num comentário do meu próprio código. 😳

Este artigo é o caminho inteiro: ler o plano ativo, listar os planos da máquina, trocar de plano e confirmar que trocou mesmo. Tudo com Node.js puro, sem instalar nada — nem um npm install. E com as armadilhas que só aparecem quando você roda de verdade. ⚡

🔌 O comando que faz tudo

A boa notícia é que o Windows já resolve isso sozinho, e o programa se chama powercfg. Ele está em qualquer instalação, desde sempre, e não precisa de instalação nenhuma. Três subcomandos dão conta do artigo inteiro:

powercfg /getactivescheme     :: qual plano esta ativo agora
powercfg /list                :: quais planos existem nesta maquina
powercfg /setactive <guid>    :: ativa um plano

Rodando o primeiro aqui, a resposta veio assim:

GUID do Esquema de Energia: 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c  (Alto desempenho)

E o /list mostra os três de sempre, com um asterisco marcando o ativo:

Esquemas de Energia Existentes (* Ativos)
-----------------------------------
GUID do Esquema de Energia: 381b4222-f694-41f0-9685-ff5bb260df2e  (Equilibrado)
GUID do Esquema de Energia: 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c  (Alto desempenho) *
GUID do Esquema de Energia: a1841308-3541-4fab-bc81-f71556f20b4a  (Economia de energia)

Repare no formato, porque ele é o mesmo nos dois comandos e é o que a gente vai extrair com uma expressão regular só: um GUID, espaços, e o nome entre parênteses. 🎯

Uma boa notícia que eu não esperava: /setactive não precisa de administrador. Rodei num terminal comum e o plano mudou. Isso muda bastante o desenho de quem quer chamar isso de dentro de um serviço.

🆔 Por que GUID, e não o nome do plano

Essa é a decisão mais importante do artigo, e a mais fácil de errar. A tentação é escrever assim:

// NAO faca isso
powercfg('/setactive', 'Economia de energia');   // ❌

Não funciona — o /setactive só aceita GUID. Mas o problema real é mais fundo: mesmo que você leia o /list e procure a linha cujo nome seja "Economia de energia", você acabou de escrever um código que só funciona em máquina com Windows em português. 🌍

No Windows em inglês, aquela mesma linha diz "Power saver". Em espanhol, "Economizador de energía". O seu if não acha nada, o script diz "plano não encontrado" e você vai passar a tarde procurando bug no lugar errado.

A saída é que os três planos padrão do Windows têm GUIDs canônicos — o mesmo número em toda instalação do mundo, em qualquer idioma:

const PLANOS = {
  economia: {
    guid: 'a1841308-3541-4fab-bc81-f71556f20b4a',
    nomes: ['Economia de energia', 'Power saver'],
  },
  equilibrado: {
    guid: '381b4222-f694-41f0-9685-ff5bb260df2e',
    nomes: ['Equilibrado', 'Balanced'],
  },
  desempenho: {
    guid: '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c',
    nomes: ['Alto desempenho', 'High performance'],
  },
};

Compare com a saída lá de cima: os três GUIDs batem exatamente. Esses números são estáveis e você pode gravá-los no código sem medo. ✅

🕳️ E quando o GUID canônico não existe na máquina?

Aqui está o motivo de aquela lista ter um campo nomes que eu ainda não usei. Se o GUID é universal, para que guardar nome?

Porque o plano pode não estar lá. 😱 Isso acontece mais do que parece:

  • Notebook de fabricante. Muita máquina sai de fábrica com planos próprios ("Modo silencioso", "Alto desempenho turbo"), com GUID gerado na hora, e alguns fabricantes removem os originais.
  • Imagem corporativa. Política de grupo apaga os planos padrão e coloca um plano único, controlado pelo TI.
  • Plano clonado. Quem cria um plano a partir de outro (powercfg /duplicatescheme) ganha um plano com o nome parecido e GUID novo.

Então a busca tem dois passos, nessa ordem: tenta o GUID canônico; se ele não estiver na lista da máquina, aí sim procura por nome — em português e em inglês:

function resolverGuid(chave) {
  const info = PLANOS[chave];
  if (!info) return null;

  const planos = listarPlanos();

  // 1) o canonico, que vale em qualquer idioma
  if (planos.some((p) => p.guid.toLowerCase() === info.guid.toLowerCase())) {
    return info.guid;
  }

  // 2) so entao o nome, para maquina com planos proprios
  for (const p of planos) {
    const bate = info.nomes.some(
      (alvo) => p.nome.toLowerCase().indexOf(alvo.toLowerCase()) !== -1
    );
    if (bate) return p.guid;
  }

  return null;   // desistiu, e isso e uma resposta valida
}

A ordem importa e não é intercambiável. Se o nome viesse primeiro, uma máquina com um plano chamado "Alto desempenho turbo" (do fabricante) faria o script ativar o plano do fabricante em vez do plano padrão do Windows — mesmo com o padrão instalado e disponível ali do lado. O GUID é preciso; o nome é o chute educado. 🎯

Repare também no indexOf(...) !== -1 em vez de igualdade: é ele que faz "Economia de energia (Fabricante)" casar com "Economia de energia". Comparação exata perderia todos os planos clonados.

E repare no return null do fim. Não encontrar é um resultado legítimo, não um caso impossível — numa máquina corporativa travada, é o resultado esperado. Quem chama precisa tratar isso, e a gente vai tratar já já.

Como eu não tenho aqui um notebook de fabricante nem uma máquina em inglês, testei essa função contra listas montadas à mão, com os três cenários:

fabricante  economia   -> 11111111-1111-1111-1111-111111111111
fabricante  desempenho -> 22222222-2222-2222-2222-222222222222
ingles      economia   -> a1841308-3541-4fab-bc81-f71556f20b4a
travada     economia   -> null

Os três caminhos funcionam: casa por nome quando o GUID sumiu, casa por GUID quando o Windows está em inglês, e devolve null — sem explodir — quando não há nada parecido. 🧪

🔤 A armadilha do acento: o TextDecoder que não conhece a codepage

Se você só for ler uma seção deste artigo, leia esta. 🙏 Foi aqui que eu descobri que estava errada.

A saída do powercfg em português tem acento, e acento em saída de programa de console do Windows é sempre uma novela. Eu sabia disso, e tinha escrito no meu código a solução que todo mundo escreve:

// O que eu tinha escrito — e que esta ERRADO
const buf = execFileSync('powercfg', args, { windowsHide: true });
return new TextDecoder('windows-1252').decode(buf);

Faz todo sentido, né? Windows, português, windows-1252. Eu tinha até um comentário orgulhoso ao lado explicando que era por causa dos acentos.

Só que o console do Windows não fala windows-1252. 🤯 Ele fala a codepage OEM, que num Windows pt-BR é a 850. E são coisas diferentes de verdade. Fui olhar os bytes crus de uma mensagem de erro do próprio powercfg:

bytes  : 506172836d6574726f7320696e76a06c69646f73
         P  a  r  ??  m  e  t  r  o  s     i  n  v ??  l  i  d  o  s

O byte 0x83 está onde deveria estar um â, e o 0xa0 onde deveria estar um á. Agora veja o que cada decodificador faz com esses mesmos bytes:

windows-1252   "Parƒmetros inv lidos -- tente digitar \"/?\" para obter ajuda"
utf-8          "Par�metros inv�lidos -- tente digitar \"/?\" para obter ajuda"
ibm850         NAO SUPORTADO pelo TextDecoder
cp850          NAO SUPORTADO pelo TextDecoder

Olha o tamanho da pegadinha. 😖 O windows-1252 não dá erro nenhum — ele decodifica felizinho e entrega "Parƒmetros". Em CP1252 o byte 0x83 realmente é ƒ e o 0xa0 é um espaço rígido. A conversão foi tecnicamente bem-sucedida e o resultado é lixo. É pior que quebrar: quebrar avisa.

E a solução óbvia — trocar por 'cp850'não existe. O TextDecoder do Node implementa a lista da especificação Encoding, e as codepages OEM da IBM não estão nela. Ele lança na hora de criar o decodificador.

A saída que eu achei mais limpa é não brigar com a decodificação e sim pedir ao Windows que fale UTF-8 desde o começo. Isso é o chcp 65001, e ele precisa rodar no mesmo processo do powercfg — por isso a chamada passa pelo cmd /c, com os dois comandos encadeados:

function powercfg(args) {
  const linha = 'chcp 65001 >nul && powercfg ' + args.join(' ');
  const buf = execFileSync('cmd', ['/c', linha], { windowsHide: true });
  return buf.toString('utf8');
}

O >nul engole o "Página de código ativa: 65001" que o chcp imprime, senão ele entraria no meio da sua saída. E o resultado, nos mesmos bytes de antes:

utf8 : "Parâmetros inválidos -- tente digitar \"/?\" para obter ajuda"
utf8 : "GUID de Subgrupos: 0012ee47-9041-4b5d-9b77-535fba8b1442  (Disco rígido)"

Acento certo, sem caractere de substituição, sem tabela de conversão na mão. 🎉

🎭 A segunda coisa que eu achava e estava errada

Eu tinha escrito no código, com toda a confiança:

// O powercfg /setactive nao imprime nada e sai com codigo 0 mesmo quando
// o GUID nao vale. Reler e a unica prova de que o plano mudou de verdade.

Parece o tipo de coisa que a gente aprende apanhando, né? Pois é. Fui provar, mandando um GUID que não existe:

--- /setactive com GUID inexistente ---
Parametros invalidos -- tente digitar "/?" para obter ajuda
lancou. status = 1

Ele lança. 😅 O execFileSync vê o código de saída 1 e joga a exceção direitinho, com a mensagem de erro no stderr. Metade do meu comentário era invenção minha — provavelmente lembrança de outro comando que se comporta assim.

Achei importante contar isso em vez de apagar e fingir que sempre soube, por dois motivos. O primeiro: "escrevi um comentário defensivo" não é o mesmo que "testei". Comentário sobre comportamento de programa externo envelhece, e às vezes já nasce errado.

O segundo é que a conclusão do comentário continua certa, mesmo com a premissa furada. Reler depois de escrever continua valendo a pena — só que por outro motivo: não para pegar erro silencioso do powercfg, e sim porque o plano de energia é estado compartilhado da máquina. O usuário pode clicar no ícone da bateria no mesmo segundo; uma política de grupo pode reverter; outro programa pode estar fazendo a mesma coisa. Reler é barato e transforma "eu mandei trocar" em "está trocado":

powercfg(['/setactive', guid]);

// Reler e barato e e a unica prova de que o plano mudou mesmo.
const depois = planoAtivo();
if (depois.guid.toLowerCase() !== guid.toLowerCase()) {
  throw new Error('o plano nao mudou: continua em "' + depois.nome + '"');
}
return { antes, depois };

Devolver { antes, depois } em vez de true é o detalhe que faz esse código ser bom de usar por outro programa: quem chama consegue registrar em log o que mudou, e consegue desfazer mais tarde, porque sabe de onde veio. Foi assim que eu devolvi esta máquina ao plano original no fim dos testes. 🔙

🧩 O script inteiro

Junta tudo: leitura, listagem, resolução de GUID com fallback, escrita com confirmação, e uma CLI simples em cima. Sem dependência nenhuma — é só salvar como plano.js e rodar.

/**
 * plano.js — le e altera o plano de energia do Windows com Node.js.
 *
 * Sem dependencia nenhuma: so o powercfg, que ja vem no Windows.
 *
 *   node plano.js                 mostra o plano ativo
 *   node plano.js listar          lista os planos desta maquina
 *   node plano.js economia        ativa Economia de energia
 *   node plano.js equilibrado     ativa Equilibrado
 *   node plano.js desempenho      ativa Alto desempenho
 */
'use strict';

const { execFileSync } = require('child_process');

// GUIDs canonicos dos tres planos do Windows. Sao os mesmos em qualquer
// instalacao e em qualquer idioma: e por isso que a busca comeca por GUID,
// e o nome so entra como plano B.
const PLANOS = {
  economia: {
    guid: 'a1841308-3541-4fab-bc81-f71556f20b4a',
    nomes: ['Economia de energia', 'Power saver'],
  },
  equilibrado: {
    guid: '381b4222-f694-41f0-9685-ff5bb260df2e',
    nomes: ['Equilibrado', 'Balanced'],
  },
  desempenho: {
    guid: '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c',
    nomes: ['Alto desempenho', 'High performance'],
  },
};

/**
 * Roda o powercfg e devolve a saida como texto.
 *
 * O `chcp 65001` nao e enfeite: o powercfg em portugues escreve na codepage
 * do console (850 nesta maquina), e o TextDecoder do Node nao conhece CP850.
 * Sem isso, "Disco rigido" volta com o acento quebrado. Mandar o Windows
 * falar UTF-8 antes resolve na origem.
 */
function powercfg(args) {
  const linha = 'chcp 65001 >nul && powercfg ' + args.join(' ');
  const buf = execFileSync('cmd', ['/c', linha], { windowsHide: true });
  return buf.toString('utf8');
}

// Toda linha de plano tem o mesmo formato:  <guid>  (Nome do plano)
const LINHA = /([0-9a-fA-F-]{36})\s*\((.+?)\)/;

/** LEITURA: o plano ativo agora, como { guid, nome, chave }. */
function planoAtivo() {
  const saida = powercfg(['/getactivescheme']);
  const m = saida.match(LINHA);
  if (!m) return { guid: '', nome: saida.trim(), chave: null };

  const ativo = { guid: m[1], nome: m[2].trim(), chave: null };

  // Descobre se o plano ativo e um dos tres conhecidos. O GUID decide;
  // o nome so e consultado para planos clonados, que tem GUID proprio.
  for (const chave of Object.keys(PLANOS)) {
    const info = PLANOS[chave];
    const mesmoGuid = ativo.guid.toLowerCase() === info.guid.toLowerCase();
    const mesmoNome = info.nomes.some(
      (n) => ativo.nome.toLowerCase().indexOf(n.toLowerCase()) !== -1
    );
    if (mesmoGuid || mesmoNome) {
      ativo.chave = chave;
      break;
    }
  }
  return ativo;
}

/** LEITURA: todos os planos desta maquina, como [{ guid, nome }]. */
function listarPlanos() {
  const planos = [];
  for (const linha of powercfg(['/list']).split(/\r?\n/)) {
    const m = linha.match(LINHA);
    if (m) planos.push({ guid: m[1], nome: m[2].trim() });
  }
  return planos;
}

/**
 * Descobre qual GUID ativar para uma chave.
 *
 * O canonico vem primeiro. Se ele nao estiver na maquina — imagem
 * corporativa que apagou os planos, notebook cujo fabricante substituiu
 * tudo por planos proprios — cai para o nome, em portugues e em ingles.
 */
function resolverGuid(chave) {
  const info = PLANOS[chave];
  if (!info) return null;

  const planos = listarPlanos();
  if (planos.some((p) => p.guid.toLowerCase() === info.guid.toLowerCase())) {
    return info.guid;
  }
  for (const p of planos) {
    const bate = info.nomes.some(
      (alvo) => p.nome.toLowerCase().indexOf(alvo.toLowerCase()) !== -1
    );
    if (bate) return p.guid;
  }
  return null;
}

/** ESCRITA: ativa o plano e RELE para confirmar. Retorna { antes, depois }. */
function ativarPlano(chave) {
  if (!PLANOS[chave]) throw new Error('plano invalido: ' + chave);

  const antes = planoAtivo();
  const guid = resolverGuid(chave);
  if (!guid) throw new Error("plano '" + chave + "' nao existe nesta maquina");

  powercfg(['/setactive', guid]);

  // Reler e barato e e a unica prova de que o plano mudou mesmo. Sem isso,
  // qualquer falha silenciosa vira um "pronto!" mentiroso no log.
  const depois = planoAtivo();
  if (depois.guid.toLowerCase() !== guid.toLowerCase()) {
    throw new Error('o plano nao mudou: continua em "' + depois.nome + '"');
  }
  return { antes, depois };
}

function main() {
  const acao = (process.argv[2] || 'status').toLowerCase();

  try {
    if (acao === 'status') {
      const p = planoAtivo();
      console.log('Plano ativo: ' + p.nome + '  [' + p.guid + ']  chave=' + p.chave);
      return;
    }

    if (acao === 'listar') {
      for (const p of listarPlanos()) console.log(p.guid + '  ' + p.nome);
      return;
    }

    if (PLANOS[acao]) {
      const r = ativarPlano(acao);
      console.log('Antes:  ' + r.antes.nome + '  [' + r.antes.guid + ']');
      console.log('Depois: ' + r.depois.nome + '  [' + r.depois.guid + ']');
      return;
    }

    console.log('Uso: node plano.js [status|listar|economia|equilibrado|desempenho]');
    process.exitCode = 1;
  } catch (e) {
    console.error('ERRO: ' + e.message);
    process.exitCode = 1;
  }
}

if (require.main === module) main();

module.exports = { planoAtivo, listarPlanos, resolverGuid, ativarPlano, PLANOS };

Dois detalhes de forma que valem o comentário:

O if (require.main === module) antes do main() é o que permite que este arquivo seja as duas coisas: um comando de terminal e um módulo. Sem ele, um require('./plano') feito de outro script imprimiria o texto de uso da CLI no meio da sua aplicação. 🙃

E o execFileSync, não o execSync: o argumento vai como array, sem passar pelo interpretador de linha de comando, o que dispensa pensar em aspas e escape. Aqui os argumentos são GUIDs que a gente mesmo escreveu, então o risco é baixo — mas é o hábito certo para quando o argumento vier de fora.

✅ Rodando de ponta a ponta

A execução real, na sequência: ler o plano, listar, ir para economia, ir para equilibrado, tentar um plano que não existe, e voltar ao original.

=== status ===
Plano ativo: Alto desempenho  [8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c]  chave=desempenho

=== listar ===
381b4222-f694-41f0-9685-ff5bb260df2e  Equilibrado
8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c  Alto desempenho
a1841308-3541-4fab-bc81-f71556f20b4a  Economia de energia

=== economia ===
Antes:  Alto desempenho  [8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c]
Depois: Economia de energia  [a1841308-3541-4fab-bc81-f71556f20b4a]

=== equilibrado ===
Antes:  Economia de energia  [a1841308-3541-4fab-bc81-f71556f20b4a]
Depois: Equilibrado  [381b4222-f694-41f0-9685-ff5bb260df2e]

=== turbo (invalido) ===
Uso: node plano.js [status|listar|economia|equilibrado|desempenho]
exit=1

=== RESTAURA desempenho ===
Antes:  Equilibrado  [381b4222-f694-41f0-9685-ff5bb260df2e]
Depois: Alto desempenho  [8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c]

E a conferência final, feita pelo próprio Windows e não pelo meu script — porque conferir o próprio trabalho com a própria ferramenta não prova grande coisa:

&gt; powercfg /getactivescheme
GUID do Esquema de Energia: 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c  (Alto desempenho)

Repare no exit=1 do caminho de erro. Um script que vai ser chamado por agendador ou por outro programa precisa sair com código diferente de zero quando falha — senão quem chamou acha que deu tudo certo. É o que o process.exitCode = 1 do catch garante. 🚦

E sim: a máquina voltou para o plano em que estava antes de eu começar. Ler o estado antes de mexer e restaurar no fim é uma gentileza básica de quem escreve código que altera configuração de sistema. 💙

📌 O que eu levo daqui

O comando era mesmo de cinco minutos. O que consumiu a tarde foram as duas suposições — e as duas estavam escritas em comentários meus, com ar de quem sabia o que estava dizendo.

Uma delas me fazia decodificar a saída na codepage errada, silenciosamente. A outra atribuía ao powercfg um defeito que ele não tem. Nenhuma das duas apareceria sem rodar o programa e olhar os bytes. 🔍

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Leia também