Battery Status API Spoofer

Either fully removes navigator.getBattery() or replaces it with a fixed 100%-charged state, to prevent fingerprinting/tracking via battery level and charging status

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Battery Status API Spoofer
// @namespace    https://violentmonkey.github.io/
// @version      1.0
// @description  Either fully removes navigator.getBattery() or replaces it with a fixed 100%-charged state, to prevent fingerprinting/tracking via battery level and charging status
// @author       Streampunk
// @icon         data:image/png;base64,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
// @match        *://*/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ===================== CONFIG =====================
  // 'block' - completely removes getBattery()/navigator.battery, so calls
  //           throw/are undefined, exactly as if the API never existed
  //           (indistinguishable from browsers that dropped it, e.g.
  //           current Firefox/Safari).
  // 'fixed' - keeps the API present but always returns a fixed, plausible
  //           100%-charged / plugged-in state instead of real battery data.
  const MODE = 'block'; // <-- change to 'fixed' if you prefer a fake value
  // ====================================================

  const hasGetBattery = typeof navigator.getBattery === 'function';
  const legacyProps = ['battery', 'mozBattery', 'webkitBattery'].filter((p) => p in navigator);

  if (!hasGetBattery && legacyProps.length === 0) {
    return; // nothing to do, API not present at all
  }

  if (MODE === 'block') {
    // --- Fully remove the API ---

    if (hasGetBattery) {
      try {
        Object.defineProperty(navigator, 'getBattery', {
          configurable: true,
          value: undefined,
        });
      } catch (e) {
        try {
          const proto = Object.getPrototypeOf(navigator);
          Object.defineProperty(proto, 'getBattery', {
            configurable: true,
            value: undefined,
          });
        } catch (e2) {
          console.warn('[block-battery] failed to remove getBattery', e2);
        }
      }
    }

    legacyProps.forEach((prop) => {
      try {
        Object.defineProperty(navigator, prop, {
          configurable: true,
          get() { return undefined; },
        });
      } catch (e) {
        console.warn(`[block-battery] failed to remove navigator.${prop}`, e);
      }
    });

    return;
  }

  if (MODE === 'fixed') {
    // --- Return a fixed, plausible battery state ---

    // Fully charged and plugged in: the most common/least informative
    // state (doesn't reveal usage patterns, discharge rate, or battery
    // health, which were previously used for cross-session tracking).
    const FAKE_BATTERY_STATE = {
      charging: true,
      chargingTime: 0,
      dischargingTime: Infinity,
      level: 1.0,
    };

    function createFakeBatteryManager() {
      const listeners = {
        chargingchange: [],
        chargingtimechange: [],
        dischargingtimechange: [],
        levelchange: [],
      };

      const state = { ...FAKE_BATTERY_STATE };

      // Minimal EventTarget-like object exposing the same interface as the
      // real BatteryManager (properties + on*change handlers + addEventListener)
      const battery = {
        get charging() { return state.charging; },
        get chargingTime() { return state.chargingTime; },
        get dischargingTime() { return state.dischargingTime; },
        get level() { return state.level; },

        onchargingchange: null,
        onchargingtimechange: null,
        ondischargingtimechange: null,
        onlevelchange: null,

        addEventListener(type, listener) {
          if (listeners[type] && typeof listener === 'function') {
            listeners[type].push(listener);
          }
        },
        removeEventListener(type, listener) {
          if (listeners[type]) {
            const idx = listeners[type].indexOf(listener);
            if (idx !== -1) listeners[type].splice(idx, 1);
          }
        },
        dispatchEvent(event) {
          // Never actually triggered since state never changes, but kept
          // for interface completeness in case a site calls it directly.
          const type = event && event.type;
          if (listeners[type]) {
            listeners[type].forEach((fn) => {
              try { fn.call(battery, event); } catch (e) {}
            });
          }
          const handlerName = 'on' + type;
          if (typeof battery[handlerName] === 'function') {
            try { battery[handlerName].call(battery, event); } catch (e) {}
          }
          return true;
        },
      };

      return battery;
    }

    const fakeBattery = createFakeBatteryManager();
    const fakeBatteryPromise = Promise.resolve(fakeBattery);

    if (hasGetBattery) {
      try {
        Object.defineProperty(navigator, 'getBattery', {
          configurable: true,
          writable: true,
          value: function () {
            return fakeBatteryPromise;
          },
        });
      } catch (e) {
        try {
          const proto = Object.getPrototypeOf(navigator);
          Object.defineProperty(proto, 'getBattery', {
            configurable: true,
            writable: true,
            value: function () {
              return fakeBatteryPromise;
            },
          });
        } catch (e2) {
          console.warn('[spoof-battery] failed to override getBattery', e2);
        }
      }
    }

    legacyProps.forEach((prop) => {
      try {
        Object.defineProperty(navigator, prop, {
          configurable: true,
          get() { return fakeBattery; },
        });
      } catch (e) {
        console.warn(`[spoof-battery] failed to override navigator.${prop}`, e);
      }
    });

    return;
  }

  console.warn('[battery-privacy-script] unknown MODE value:', MODE);
})();