npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

signalk-alpicool

v0.4.0

Published

Signal K plugin that monitors and controls an Alpicool (Brass Monkey / white-label K25-board) Bluetooth compressor fridge/freezer over BLE — publishes temperature, target setpoint, battery voltage and power/Eco-Max state to Signal K, and lets a Signal K c

Downloads

972

Readme

signalk-alpicool

A Signal K node-server plugin that monitors and controls an Alpicool Bluetooth compressor fridge/freezer (and its rebrands — Brass Monkey and other white-label coolers built on the same "K25" board) directly from the server's own Bluetooth adapter.

It reads temperature, target setpoint, battery voltage and power/Eco-Max state off the fridge and publishes them as Signal K deltas, and it lets any Signal K client set the target temperature and turn the fridge on/off over BLE.

  • No extra hardware — talks to the fridge from the Pi/host BLE adapter.
  • No pairing and no PIN — the Alpicool BLE protocol is unauthenticated.
  • Uses the Web Bluetooth API (webbluetooth/ SimpleBLE — BlueZ over DBus on Linux), so it works inside a Dockerised Signal K server that mounts /run/dbus + /var/run/dbus (no raw HCI socket / CAP_NET_RAW / root needed).

What it publishes

SI units, emitted as deltas (with units meta). Paths use configurable prefixes (refrigerator default environment.inside.refrigerator, freezer default environment.inside.freezer) and a battery id (default fridge):

| Signal K path | Units | Meaning | |---|---|---| | environment.inside.refrigerator.temperature | K | Measured temperature — warmer zone | | environment.inside.refrigerator.targetTemperature | K | Setpoint, warmer zone (settable — see Control) | | environment.inside.freezer.temperature | K | Measured temperature — colder zone (dual-zone units) | | environment.inside.freezer.targetTemperature | K | Setpoint, colder zone (settable) | | environment.inside.refrigerator.state | — | on / off (soft power state; settable) | | environment.inside.refrigerator.mode | — | eco / max (run mode) | | electrical.batteries.fridge.voltage | V | Input/battery voltage the fridge senses | | electrical.batteries.fridge.capacity.stateOfCharge | ratio | Battery charge (0–1) as reported by the fridge |

Dual-zone units publish both zones. Zones are labelled by value, not by physical side: the lower-setpoint zone is the freezer, the higher the refrigerator — so the paths stay meaningful whichever way round the unit numbers its compartments. Single-zone units publish only the refrigerator paths.

The fridge reports whole degrees in its own display unit (°C or °F); the plugin converts to Kelvin for Signal K. The unit is read from the fridge's status; a fallback is set in config until the first status arrives.

Control (PUT)

When Enable control is on, the plugin registers PUT handlers so a Signal K client can write the fridge over BLE:

  • refrigerator.targetTemperature / freezer.targetTemperature — PUT a value in Kelvin → the plugin converts to the fridge's unit and sends a Set-Target frame. Each label is mapped back to the correct physical zone (BLE command 0x05 / 0x06) from the current status, so setting "freezer" always hits the colder compartment.
  • …​.state — PUT on/off (or true/false) → the plugin sends a bulk Set frame (BLE command 0x02) that flips soft power without disturbing other settings.

Example (Signal K PUT over HTTP):

PUT /signalk/v1/api/vessels/self/environment/inside/refrigerator/targetTemperature
{ "value": 277.15 }      # 4 °C

With control disabled the plugin is strictly read-only.

Resilience & plugin status

BLE links to these fridges fail in three distinct ways, and the plugin defends against all of them:

  • The link drops (fridge out of range, adapter reset, …) — the transport reconnects with exponential backoff (3 s → 60 s), retrying the connect around the fridge's intermittent advertising, with every scan/connect/GATT call under a timeout so a hung BLE promise can never wedge the plugin.
  • The link stays up but goes silent — the socket looks connected while the fridge has stopped answering. A data watchdog guards this: the plugin records when every status frame arrives, and if nothing lands for 3× the poll interval (the extra beyond one interval is capped at 5 minutes, so a long poll interval can't quietly disable the protection) it declares the link dead regardless of socket state, forces a disconnect and lets the reconnect loop recover. After 3 forced reconnects with no data in between the plugin goes into an error state so the problem is visible on the server dashboard; it returns to normal as soon as a reading arrives.
  • A previous run left the link open — BlueZ runs on the host and outlives the plugin process (especially in Docker, where it is reached over the mounted D-Bus socket). If the process is killed rather than stopped cleanly, BlueZ keeps holding the fridge's single BLE connection — and a connected device does not advertise, so every scan after that times out forever while bluetoothctl info shows Connected: yes. The plugin self-heals: before every scan it asks BlueZ directly (over D-Bus, no extra dependencies) whether the fridge is already connected on this host, releases the orphaned link, and says so in the status. plugin.stop() also releases the link deterministically, so an ordinary restart never orphans it in the first place. This is safe by construction: BlueZ only knows links made through this host's own adapter, so the self-heal can never kick the owner's phone off the fridge.

The plugin status line (Server → Plugin Config / Dashboard) reports data freshness, not just socket state — e.g. Connected — last reading 30s ago, Connected but silent for 2min — forced reconnect (1/3), or Disconnected — reconnecting (last reading 5min ago). A healthy-looking status can therefore be trusted: it goes stale when the data does.

The fridge accepts only ONE Bluetooth connection

This is a hardware constraint, not a plugin setting: while the Alpicool phone app (or any other device) is connected, the plugin cannot connect at all — and vice versa. When connect attempts keep failing, the plugin status says so explicitly and suggests closing the phone app. If readings stop while everything looks fine, the phone app being open nearby is the first thing to check.

The same constraint has a second face: a stale link on the Signal K host itself (orphaned by a killed process — see above). The two are distinguishable and the status names each one: a host-side link shows Connected: yes in bluetoothctl info <MAC> and is released automatically by the self-heal; a phone holding the fridge is invisible to the host's BlueZ and simply makes every scan and connect fail until the app is closed. If the self-heal ever cannot release a host-side link, the status says so and gives the manual fallback: bluetoothctl disconnect <MAC>.

Finding the fridge's BLE MAC address

The fridge advertises a BLE name of WT-0001, or one starting with A1- / AK1- / AK2- / AK3-. On the Signal K host (e.g. a Raspberry Pi):

bluetoothctl
  scan on          # watch for WT-0001 / A1-* / AK*  and note its MAC
  scan off
  exit

Put that MAC (e.g. E4:5F:01:AA:BB:CC) into the plugin config. You do not need to pair or trust the device — the plugin connects directly.

Install

Install the supported way — via the Signal K Appstore (search "alpicool"), or with npm into your Signal K server:

npm install signalk-alpicool

Then enable and configure it in Server → Plugin Config. Do not hand-copy the plugin into node_modules. See INSTALL.md for Docker/DBus specifics and troubleshooting.

Configuration

| Setting | Default | Notes | |---|---|---| | Fridge BLE MAC address | — | Required. AA:BB:CC:DD:EE:FF. | | Poll interval (seconds) | 60 | How often to query status (BLE 0x01). Min 5. Fridge temps move slowly — once a minute is plenty, and fewer polls mean less traffic on the fridge's single BLE connection. | | Signal K path prefix — refrigerator (warmer zone) | environment.inside.refrigerator | Base for the warmer zone + state/mode. | | Signal K path prefix — freezer (colder zone) | environment.inside.freezer | Dual-zone units only; base for the colder zone. | | Battery id | fridge | electrical.batteries.<id> for voltage + state of charge. | | Fridge temperature unit (fallback) | C | Used until the first status reveals the real unit. | | Enable control (PUT → BLE) | off | Turn on to allow setting target/power over BLE. |

Protocol

The Alpicool/Brass Monkey GATT protocol is a BLE-to-serial bridge: service 0x1234, write characteristic 0x1235, notify characteristic 0x1236; FE FE-framed binary commands with a 16-bit sum checksum. The codec in lib/protocol.js is ported from klightspeed/BrassMonkeyFridgeMonitor (Python) and cross-checked against johnelliott/alpicoold (Go). The codec has deterministic unit tests (npm test, no Bluetooth required).

Status

Verified live on a dual-zone Alpicool: reads both zones + battery, and control (set target on either zone, power on/off) works end-to-end over BLE. Codec has deterministic unit tests (npm test, no Bluetooth). See the protocol gotchas below.

License

MIT — see LICENSE.

Protocol gotchas (verified on real hardware, 2026-07-20)

Three things are essential and easy to miss (all cost real debugging time):

  1. BIND is a fallback, not a handshake. Some units answer QUERY straight away with no BIND (0x00) ever sent; others stay silent until one is accepted (the display shows APP and you press the pairing button once). Sending BIND unconditionally on every connect nags the owner at the fridge on each reconnect — so this plugin polls first and only sends a single BIND if no status arrives within 6 s. (Ref: Gruni22/alpicool_ha_ble; fallback behaviour verified on a real K25.)
  2. Write-without-response. The write characteristic 0x1235 only permits write-without-response (command). Using write-with-response (request) fails with Write not permitted and every poll silently drops.
  3. Fragmented + concatenated notifications. A status arrives split across BLE notifications at the ATT MTU, and a single notification can concatenate an echo + a status. Reassemble by the FE FE <len> frame length (this plugin does).

Verified live on a dual-zone Alpicool (BLE name A1-…): temp/target/battery/state all decode and publish to Signal K.