Powerpal smart energy monitor

Big update to the Powerpal ESPHome component — battery sensor fixed, more reliable, new features

Been doing a proper pass over this project recently and there’s a lot to share. If you’re running this component, it’s worth updating — several real bugs are fixed and a few new things are worth turning on.

What’s fixed:

  • The battery sensor was silently broken — it never actually read the battery level from the device. It works properly now.
  • Reconnects after a BLE dropout are faster and more reliable (removed a race that could stack up duplicate reconnect attempts).
  • Power readings used to go blank for a while after any reconnect — they now come back immediately, with a sanity check that skips the calculation only for a genuinely long outage rather than showing a garbage number.
  • Fixed compiler warnings/errors some people were hitting on newer ESPHome versions.
  • Fixed a web_server auth deprecation warning.
  • Cleaned out a chunk of old dead code and a couple of unnecessary BLE round-trips on every reconnect.

What’s new:

  • A LED Sensitivity sensor (shows the device’s pulse-detection sensitivity setting).
  • A button to retrieve your Powerpal’s API key and device ID straight into Home Assistant — no more digging through logs.
  • An optional automatic RTC sync for the Powerpal’s onboard clock (syncs on boot and every 6 hours).
  • A documented way to disable WiFi power-saving (power_save_mode: none), which can help BLE reliability since the ESP32 shares one radio for both.

Also cleaned up: the README no longer references cloud-uploading as a working feature (it never actually was — if you want your data in the Powerpal app too, there’s now a documented Home Assistant rest_command approach that actually works, using the API key/device ID this component already exposes).

Credit where it’s due — a few things came from the community, not just from me:

  • CV8R — found the LED Sensitivity characteristic UUID (in PR #68), which is what made that new sensor possible.
  • SleepinDevil — the original idea for retrieving the API key via a getter, and the RTC time-sync button pattern.
  • skinnybuddah — packaged that into a proper automatic sync script, and flagged the power_save_mode tip.
  • elysiandc — correctly diagnosed a real compile error in PR #58 (already independently fixed a different way by the time I got to it, but the diagnosis was right).
  • BenBarat and craigmate — reported the compile-warning and battery issues that are now fixed.

How to update: point your external_components at ref: "main" instead of a pinned version tag, so you get fixes as they land rather than waiting on a tagged release:

esphome:
  name: powerpal-gateway
  friendly_name: Powerpal Gateway
  on_boot:
    priority: -100
    then:
      - script.execute: sync_powerpal_time

esp32:
  board: esp32dev
  framework:
    type: esp-idf

external_components:
  - source:
      type: git
      url: https://github.com/gurrier/esphome-powerpal_ble.git
      ref: "main"   # or a specific version tag, e.g. "1.6.1", to pin to a stable release
    components: [ powerpal_ble ]

time:
  - platform: homeassistant
    id: homeassistant_time

ble_client:
  - mac_address: XX:XX:XX:XX:XX:XX # << EDIT HERE
    id: powerpal

button:
  - platform: restart
    name: "Restart"
    icon: "mdi:restart"
  - platform: template
    name: "Retrieve Powerpal API Key and Device ID"
    on_press:
      then:
        - lambda: |-
            id(powerpal_api_key).publish_state(id(powerpal_ble_sensor).get_apikey());
            id(powerpal_device_id).publish_state(id(powerpal_ble_sensor).get_device_id());

text_sensor:
  - platform: template
    name: "Powerpal API Key"
    id: powerpal_api_key
  - platform: template
    name: "Powerpal Device ID"
    id: powerpal_device_id

script:
  - id: sync_powerpal_time
    mode: restart
    then:
      # Wait for Home Assistant time (max 30s)
      - wait_until:
          condition:
            lambda: 'return id(homeassistant_time).now().is_valid();'
          timeout: 30s

      # Wait for BLE connection (max 30s)
      - wait_until:
          condition:
            lambda: 'return id(powerpal).connected();'
          timeout: 30s

      # Only sync if both are ready
      - if:
          condition:
            lambda: |-
              return id(homeassistant_time).now().is_valid() &&
                     id(powerpal).connected();
          then:
            - logger.log: "Syncing Powerpal RTC time"
            - ble_client.ble_write:
                id: powerpal
                service_uuid: '59DAABCD-12F4-25A6-7D4F-55961DCE4205'
                characteristic_uuid: '59DA0004-12F4-25A6-7D4F-55961DCE4205'
                value: !lambda |-
                  uint32_t t = id(homeassistant_time).now().timestamp;
                  return std::vector<uint8_t>{
                    (uint8_t)(t), (uint8_t)(t >> 8),
                    (uint8_t)(t >> 16), (uint8_t)(t >> 24)
                  };
          else:
            - logger.log: "Skipping RTC sync (timeout or not connected)"
            - delay: 2min
            - script.execute: sync_powerpal_time

interval:
  - interval: 6h
    then:
      - script.execute: sync_powerpal_time

sensor:
  - platform: powerpal_ble
    id: powerpal_ble_sensor
    ble_client_id: powerpal
    power:
      name: "Powerpal Power"
    battery_level:
      name: "Powerpal Battery"
    led_sensitivity:
      name: "Powerpal LED Sensitivity"
    daily_energy:
      name: "Powerpal Daily Energy"
    daily_pulses:
      name: "Powerpal Daily Pulses"
    energy:
      name: "Powerpal Total Energy"
    watt_hours:
      name: "Powerpal Watt Hours"
    pulses:
      name: "Powerpal Pulses"
    timestamp:
      name: "Powerpal Timestamp"
    cost:
      name: "Powerpal Cost"
    pairing_code: 123456  # << EDIT HERE
    notification_interval: 1 # In seconds
    pulses_per_kwh: 1000
    time_id: homeassistant_time

logger:
#  level: DEBUG
#  logs:
#    powerpal_ble: DEBUG

api:
  encryption:
    key: !secret esphome_api_encryption_key

ota:
  platform: esphome
  password: "XXXXXXXXXXXXXXXXXXXXX" # << EDIT HERE

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none # avoid WiFi power-saving interfering with BLE timing/reliability
  ap:
    ssid: "Powerpal Fallback Hotspot"
    password: "XXXXXXXX" # << EDIT HERE

web_server:
  port: 80
  auth:
    type: digest
    username: !secret esphome_web_username
    password: !secret esphome_web_password

Repo’s here if you want to look at the code or file an issue: GitHub - gurrier/esphome-powerpal_ble · GitHub

Powerpal uploader Home Assistant rest_command process is a bit hard to find.
edit configuration.yaml with:

rest_command:
  powerpal_upload:
    url: https://readings.powerpal.net/api/v1/meter_reading/<powerpal_device_id> <----EDIT DEVICE ID
    method: POST
    headers:
      authorization: "AUTHORISATION KEY" <-----EDIT. eg d1eb6447-377e-4fb0-8308-835c01234
      accept: "application/json"
    content_type: "application/json"
    payload: >-
      [
        {
          "cost": {{ states('sensor.study_somfy_powerpal_cost') | float(0) }},
          "is_peak": false,
          "pulses": {{ states('sensor.study_somfy_powerpal_pulses') | int(0) }},
          "timestamp": {{
            (states('sensor.study_somfy_powerpal_timestamp') | int(0) // 60) * 60
          }},
          "watt_hours": {{ states('sensor.study_somfy_powerpal_watt_hours') | float(0) }}
        }
      ]
    content_type: "application/json"
    verify_ssl: true

Create an automation that uploads every minute:
Home Assistant:
Settings/ Automations an Scenes → Create automation button.
Create new automation
Select edit in Yaml in top right hand corner (under 3 dots)
Yaml:

alias: Powerpal Cloud Upload
description: Upload Powerpal readings every minute
triggers:
  - minutes: /1
    trigger: time_pattern
conditions: []
actions:
  - action: rest_command.powerpal_upload
mode: single

To test go to Setting/Tools.
Select Actions then YAML mode.
paste in field and Perform action.

action: rest_command.powerpal_upload
data: {}

You should get a response like:

content:
  readings:
    - timestamp: 1788747000
      pulses: 10
      watt_hours: 10
      cost: 0.0006798611
      usage_cost: 0
      fixed_cost: 0.0006798611
      cost_per_kwh: 0.19437
      is_peak: false
      is_final: true
      samples: 1
status: 200
headers:
  Access-Control-Allow-Headers: Authorization, Content-Type, Content-Encoding, Accept, Accept-Encoding
  Access-Control-Allow-Methods: '*'
  Access-Control-Allow-Origin: '*'
  Content-Type: application/json
  Strict-Transport-Security: max-age=31536000;
  Vary: Accept-Encoding
  Date: Mon, 07 Sep 2026 02:15:30 GMT
  Content-Length: '292'
  Via: 1.1 google
  Alt-Svc: h3=":443"; ma=2592000

status: 200 means its authenticated ok.

havent had issues on main so 1.4 seems to be working well

Go for latest version 1.6.3 now