@rhizomatics/signalk-ble-sensors-plugin 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
+ # [1.0.2]
2
+
3
+ - SwitchBot Meter Plus: temperature was wrong when the meter's display was set to Fahrenheit (e.g. 22.7°C published as -5.2°C). The broadcast value is always Celsius; the flag only says which unit the display shows
4
+ - Xiaomi ATC: sensors using the `atc1441` advertising format were decoded with the pvvx layout and published nonsense temperatures. Both formats are now decoded, and BTHome payloads (not supported yet) are no longer misread as either
5
+ - **Path change:** humidity is now published to `environment.<zone>.relativeHumidity`, the SignalK spec path (`humidity` is deprecated in the spec). Update any dashboard or alarm that reads `environment.<zone>.humidity`
6
+ - Zone examples in the config screen and README now follow the spec's `inside.<zone>` / `outside` layout. Existing zones are unchanged
7
+
1
8
  # [1.0.1]
9
+
2
10
  No code change, re-release only for automated build.
3
11
 
4
12
  # [1.0.0]
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # signalk-ble-sensors-plugin
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@rhizomatics/signalk-ble-sensors-plugin.svg)](https://www.npmjs.com/package/@rhizomatics/signalk-ble-sensors-plugin)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@rhizomatics/signalk-ble-sensors-plugin.svg)](https://www.npmjs.com/package/@rhizomatics/signalk-ble-sensors-plugin)
5
+ [![code style: oxfmt](https://img.shields.io/badge/code_style-oxfmt-blue.svg)](https://github.com)
6
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/rhizomatics/signalk-ble-sensors-plugin./blob/main/LICENSE)
7
+ [![boat tech directory](https://boat-tech-directory.rhizomatics.org.uk/images/badge.svg)](https://boat-tech-directory.rhizomatics.org.uk)
8
+
3
9
  This is a limited replacement for `bt-sensors-plugin-sk` that has SignalK BLE Manager support, with only a few sensors known to work and passive only BLE integration. It may work for other sensors not listed here, and PRs welcome to add or correct support.
4
10
 
5
11
  Passive BLE sensor listening for [SignalK](https://signalk.org/) — reads temperature, humidity and battery data broadcast by cheap, popular BLE sensors and republishes it to SignalK paths, without ever opening a Bluetooth connection to the device. Relies only on _BLE Manager_, so requires SignalK v2.31 or above with v2.34+ recommended.
@@ -29,7 +35,7 @@ For boats that have IP68 RuuviTag Pro outside and RuuviTag inside, it can be han
29
35
  | Xiaomi LYWSD03MMC (pvvx/atc1441 custom firmware) | Best-effort — not actively used, contributions welcome |
30
36
  | Govee H5074 / H5075 | Best-effort — not actively used, contributions welcome |
31
37
 
32
- Stock Xiaomi firmware (encrypted, not the ATC/pvvx custom firmware above) isn't supported. Sensors that need a GATT connection to read (Victron, Renogy, most BMS chips, etc.) are out of scope for this plugin. Many of these already have plugins, such as [signalk-bluetti-plugin](https://github.com/rhizomatics/signalk-bluetti-plugin) or [signalk-victron-ble](https://github.com/stefanor/signalk-victron-ble).
38
+ Stock Xiaomi firmware (encrypted, not the ATC/pvvx custom firmware above) isn't supported. On the custom firmware, choose the `custom` (pvvx) or `atc1441` advertising format; `BTHome` isn't decoded yet. Sensors that need a GATT connection to read (Victron, Renogy, most BMS chips, etc.) are out of scope for this plugin. Many of these already have plugins, such as [signalk-bluetti-plugin](https://github.com/rhizomatics/signalk-bluetti-plugin) or [signalk-victron-ble](https://github.com/stefanor/signalk-victron-ble).
33
39
 
34
40
  ## Configuring a sensor
35
41
 
@@ -37,7 +43,7 @@ Stock Xiaomi firmware (encrypted, not the ATC/pvvx custom firmware above) isn't
37
43
  2. Make sure the sensor is powered on and within Bluetooth range for a minute or two.
38
44
  3. Open the plugin's configuration page and click **+ Add Item** under "Registered sensors".
39
45
  4. Pick the sensor from the **Device** dropdown — it lists every recognised sensor the plugin has seen, labelled with its advertised name, sensor type, and MAC address. If it's not there yet, close the page, wait a bit longer, and reopen it.
40
- 5. Fill in a **Zone / location** (e.g. `cabin`, `fridge`, `engine`) — this becomes part of the SignalK path (`environment.<zone>.temperature`, etc.); dots nest into sub-paths, e.g. `inside.mainCabin` becomes `environment.inside.mainCabin.temperature` — and optionally a **Name override**.
46
+ 5. Fill in a **Zone / location** — this becomes part of the SignalK path (`environment.<zone>.temperature`, etc.). The SignalK spec puts indoor zones under `inside`, so use e.g. `inside.mainCabin`, `inside.refrigerator` or `inside.engineRoom` (giving `environment.inside.mainCabin.temperature`), and `outside` for the outside air — and optionally a **Name override**.
41
47
  6. Save. Readings should start appearing within a few seconds.
42
48
 
43
49
  If a sensor measures pressure (currently RuuviTag only), it can optionally also be picked as the **Outside pressure source**
@@ -47,16 +53,16 @@ sensor still needs to be added under "Registered sensors" as usual; picking it h
47
53
 
48
54
  ## Published paths
49
55
 
50
- | Value | Path | Unit |
51
- | ---------------- | --------------------------------- | ----------------------------------- |
52
- | Temperature | `environment.<zone>.temperature` | K |
53
- | Humidity | `environment.<zone>.humidity` | ratio (0-1) |
54
- | Pressure | `environment.<zone>.pressure` | Pa |
55
- | Battery strength | `sensors.<name>.battery.strength` | ratio (0-1), with low-battery zones |
56
- | Battery voltage | `sensors.<name>.battery.voltage` | V |
57
- | Signal strength | `sensors.<name>.RSSI` | dB |
58
- | Reachable | `sensors.<name>.reachable` | boolean |
59
- | Motion counter | `sensors.<name>.motionCounter` | count |
56
+ | Value | Path | Unit |
57
+ | ---------------- | ------------------------------------- | ----------------------------------- |
58
+ | Temperature | `environment.<zone>.temperature` | K |
59
+ | Humidity | `environment.<zone>.relativeHumidity` | ratio (0-1) |
60
+ | Pressure | `environment.<zone>.pressure` | Pa |
61
+ | Battery strength | `sensors.<name>.battery.strength` | ratio (0-1), with low-battery zones |
62
+ | Battery voltage | `sensors.<name>.battery.voltage` | V |
63
+ | Signal strength | `sensors.<name>.RSSI` | dB |
64
+ | Reachable | `sensors.<name>.reachable` | boolean |
65
+ | Motion counter | `sensors.<name>.motionCounter` | count |
60
66
 
61
67
  `<name>` is the sensor's name, lowercased and sanitized — give each registered sensor a distinct name, since
62
68
  two sensors with the same name will collide on these paths. Not every sensor publishes every path above — only the values its
package/index.js CHANGED
@@ -140,14 +140,22 @@ function buildSchema(recognized, configured = [], outsidePressureSourceMac = nul
140
140
  type: "string",
141
141
  title: "Name override",
142
142
  description:
143
- "Used as the SignalK $source and in generated sensor paths. Defaults to the device's advertised name if it has one, otherwise its sensor type (e.g. 'SwitchBot Meter') — worth setting explicitly.",
143
+ "Used in sensors.<name>.* paths (the $source is the plugin id plus the sensor type). Defaults to the device's advertised name if it has one, otherwise its sensor type (e.g. 'SwitchBot Meter') — worth setting explicitly.",
144
144
  },
145
145
  zone: {
146
146
  type: "string",
147
147
  title: "Zone / location",
148
148
  description:
149
- "Used in environment.<zone>.* paths, e.g. 'cabin', 'engine', 'fridge'. Dots nest into sub-paths, e.g. 'inside.mainCabin' becomes environment.inside.mainCabin.*.",
150
- examples: ["inside", "outside", "galley", "fridge", "cabin", "engine", "deck", "cockpit", "inside.mainCabin"],
149
+ "Used in environment.<zone>.* paths. The SignalK spec puts indoor zones under 'inside', e.g. 'inside.mainCabin' becomes environment.inside.mainCabin.*; use 'outside' for the outside air.",
150
+ examples: [
151
+ "outside",
152
+ "inside",
153
+ "inside.mainCabin",
154
+ "inside.galley",
155
+ "inside.refrigerator",
156
+ "inside.freezer",
157
+ "inside.engineRoom",
158
+ ],
151
159
  },
152
160
  enabled: { type: "boolean", title: "Enabled", default: true },
153
161
  },
@@ -19,15 +19,14 @@ function identify(adv) {
19
19
  function decode(adv) {
20
20
  const sd = adv.serviceData.get(SERVICE_DATA_UUID);
21
21
 
22
- // Byte 4 bit 7: sign (1 = positive). Byte 4 bits 0-6: integer part.
23
- // Byte 3 bits 0-3: fractional decimal digit. Byte 5 bit 7: unit flag
24
- // (1 = the value above is in Fahrenheit rather than Celsius).
22
+ // Byte 4 bit 7: sign (1 = positive). Byte 4 bits 0-6: integer part (degC).
23
+ // Byte 3 bits 0-3: fractional decimal digit. Byte 5 bit 7 is the meter's
24
+ // display-unit setting (1 = shows Fahrenheit); the broadcast value is always
25
+ // Celsius whatever the display shows, as in pySwitchbot's decode_temp_humidity.
25
26
  const sign = (sd[4] & 0x80) !== 0 ? 1 : -1;
26
27
  const intPart = sd[4] & 0x7f;
27
28
  const frac = (sd[3] & 0x0f) / 10;
28
- const rawTemp = sign * (intPart + frac);
29
- const isFahrenheit = (sd[5] & 0x80) !== 0;
30
- const tempC = isFahrenheit ? (rawTemp - 32) / 1.8 : rawTemp;
29
+ const tempC = sign * (intPart + frac);
31
30
 
32
31
  const humidity = (sd[5] & 0x7f) / 100;
33
32
  const battStrength = sd[2] / 100;
@@ -3,37 +3,49 @@
3
3
  // Xiaomi LYWSD03MMC / similar running the pvvx or atc1441 custom firmware
4
4
  // (https://github.com/pvvx/ATC_MiThermometer) advertises under its own name
5
5
  // (ATC_XXXXXX, where XXXXXX is the last 3 MAC bytes) with a fixed-format
6
- // service-data payload — no encryption key or GATT connection needed, unlike
7
- // stock Xiaomi firmware. This decodes the firmware's default little-endian
8
- // "custom" format: mac[6] temp[i16LE, x0.01 degC] humidity[u16LE, x0.0001]
9
- // voltage[u16LE, mV] battery[u8, %] counter[u8].
6
+ // 0x181A service-data payload - no encryption key or GATT connection needed,
7
+ // unlike stock Xiaomi firmware. Two layouts share that UUID, told apart by length:
8
+ //
9
+ // - pvvx "custom" (15 bytes, little-endian): mac[6] temp[i16, x0.01 degC]
10
+ // humidity[u16, x0.01 %] voltage[u16, mV] battery[u8, %] counter[u8] flags[u8]
11
+ // - atc1441 (13 bytes, big-endian): mac[6] temp[i16, x0.1 degC] humidity[u8, %]
12
+ // battery[u8, %] voltage[u16, mV] counter[u8]
13
+ //
14
+ // The firmware's BTHome option (UUID 0xFCD2) is a different, TLV-encoded format
15
+ // and isn't decoded here.
10
16
 
11
17
  const NAME_PATTERN = /^ATC_[A-Fa-f0-9]{6}$/;
12
- const SERVICE_DATA_UUIDS = ["0000181a-0000-1000-8000-00805f9b34fb", "0000fcd2-0000-1000-8000-00805f9b34fb"];
13
-
14
- function getServiceData(adv) {
15
- for (const uuid of SERVICE_DATA_UUIDS) {
16
- const sd = adv.serviceData.get(uuid);
17
- if (sd) return sd;
18
- }
19
- return null;
20
- }
18
+ const SERVICE_DATA_UUID = "0000181a-0000-1000-8000-00805f9b34fb";
19
+ const PVVX_LENGTH = 15;
20
+ const ATC1441_LENGTH = 13;
21
21
 
22
22
  function identify(adv) {
23
23
  if (!NAME_PATTERN.test(adv.name)) return false;
24
- const sd = getServiceData(adv);
25
- return !!sd && sd.length >= 13;
24
+ const sd = adv.serviceData.get(SERVICE_DATA_UUID);
25
+ return !!sd && (sd.length === ATC1441_LENGTH || sd.length >= PVVX_LENGTH);
26
26
  }
27
27
 
28
- function decode(adv) {
29
- const sd = getServiceData(adv);
28
+ function decodePvvx(sd) {
29
+ return {
30
+ temp: 273.15 + sd.readInt16LE(6) / 100,
31
+ humidity: sd.readUInt16LE(8) / 10000,
32
+ battVoltage: sd.readUInt16LE(10) / 1000,
33
+ battStrength: sd.readUInt8(12) / 100,
34
+ };
35
+ }
30
36
 
31
- const temp = 273.15 + sd.readInt16LE(6) / 100;
32
- const humidity = sd.readUInt16LE(8) / 10000;
33
- const battVoltage = sd.readUInt16LE(10) / 1000;
34
- const battStrength = sd.readUInt8(12) / 100;
37
+ function decodeAtc1441(sd) {
38
+ return {
39
+ temp: 273.15 + sd.readInt16BE(6) / 10,
40
+ humidity: sd.readUInt8(8) / 100,
41
+ battStrength: sd.readUInt8(9) / 100,
42
+ battVoltage: sd.readUInt16BE(10) / 1000,
43
+ };
44
+ }
35
45
 
36
- return { temp, humidity, battVoltage, battStrength };
46
+ function decode(adv) {
47
+ const sd = adv.serviceData.get(SERVICE_DATA_UUID);
48
+ return sd.length >= PVVX_LENGTH ? decodePvvx(sd) : decodeAtc1441(sd);
37
49
  }
38
50
 
39
51
  module.exports = {
@@ -12,9 +12,9 @@ const FIELD_PATHS = {
12
12
  description: "Zone's current temperature",
13
13
  },
14
14
  humidity: {
15
- path: "environment.{zone}.humidity",
15
+ path: "environment.{zone}.relativeHumidity",
16
16
  unit: "ratio",
17
- description: "Zone's current humidity",
17
+ description: "Zone's current relative humidity",
18
18
  },
19
19
  pressure: {
20
20
  path: "environment.{zone}.pressure",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-ble-sensors-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Passive BLE sensor listening for SignalK (SwitchBot, RuuviTag, and others) via the SignalK BLE Manager API — no GATT connection required.",
5
5
  "keywords": [
6
6
  "ble",