Skip to content

Commit de6e96d

Browse files
committed
Added BTHome library
1 parent 373c01f commit de6e96d

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed

modules/BTHome.jpg

49.3 KB
Loading

modules/BTHome.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* Copyright (c) 2023 Gordon Williams. See the file LICENSE for copying permission. */
2+
/*
3+
Module for creating BTHome.io compatible Advertisements
4+
5+
https://www.espruino.com/BTHome
6+
*/
7+
8+
exports.packetId = 0;
9+
10+
exports.getAdvertisement = function(devices) {
11+
const b16 = (id,v)=>[id,v&255, (v>>8)&255];
12+
const b24 = (id,v)=>[id,v&255, (v>>8)&255, (v>>16)&255];
13+
const b32 = (id,v)=>[id,v&255, (v>>8)&255, (v>>16)&255, (v>>24)&255];
14+
15+
const DEV = {
16+
battery : e => [ 1, E.clip(Math.round(e.v),0,100)], // 0..100, int
17+
temperature : e => b16(2, Math.round(e.v*100)), // degrees C, floating point
18+
count : e => [ 0x0F, e.v], // 0..255, int
19+
count16 : e => b16(0x3D, e.v), // 0..65535, int
20+
count32 : e => b32(0x3E, e.v), // 0..0xFFFFFFFF, int
21+
current : e => b16(0x3D, Math.round(e.v*1000)), // amps, floating point
22+
duration : e => b16(0x42, Math.round(e.v*1000)), // seconds, floating point
23+
humidity : e => [0x2E, Math.round(e.v)], // humidity %, int
24+
humidity16 : e => b16(3, Math.round(e.v*100)), // humidity %, floating point
25+
power : e => b24(0x0B, Math.round(e.v*100)), // power (W?), floating point
26+
pressure : e => b24(4, Math.round(e.v*100)), // pressure (hPa), floating point
27+
voltage : e => b16(0x0C, Math.round(e.v*1000)), // voltage (V), floating point
28+
text : e => { let t = ""+e.v; return [ 0x53, t.length ].concat(t.split("").map(c=>c.charCodeAt())); }, // text string
29+
button_event : e => {
30+
const events=["none","press","double_press","triple_press","long_press","long_double_press","long_triple_press"];
31+
if (!events.includes(e.v)) throw new Error(`Unknown event type ${E.toJS(e.v)}`);
32+
return [0x3A, events.indexOf(e.v)];
33+
},
34+
dimmer_event : e => {
35+
var n = 0;
36+
if (e.v<0) return [0x3C, 1, -e.v]; // left
37+
if (e.v>0) return [0x3C, 2, e.v]; // right
38+
return [0x3C, 0, 0];
39+
}
40+
};
41+
const BOOL = {
42+
battery_low : 0x15,
43+
battery_charge : 0x16,
44+
cold : 0x18,
45+
connected : 0x19,
46+
door : 0x1A,
47+
garage_door : 0x1B,
48+
boolean : 0x0F,
49+
heat : 0x1D,
50+
light : 0x1E,
51+
locked : 0x1F,
52+
motion : 0x21,
53+
moving : 0x22,
54+
occupancy : 0x23,
55+
opening : 0x11,
56+
power_on : 0x10,
57+
presence : 0x25,
58+
problem : 0x26,
59+
tamper : 0x2B
60+
};
61+
62+
exports.packetId = (exports.packetId+1)&255;
63+
let adv = [
64+
/* BTHome Device Information
65+
bit 0: "Encryption flag"
66+
bit 1: "Reserved for future use"
67+
bit 2: Trigger based device flag (0 = we advertise all the time)
68+
bit 1: "Reserved for future use"
69+
bit 5-7: "BTHome Version" = 2 */
70+
0x40,
71+
/* Packet ID - by only changing this */
72+
0, exports.packetId,
73+
];
74+
adv = adv.concat.apply(adv,
75+
devices.map(dev => {
76+
if (dev.type in DEV) return DEV[dev.type](dev);
77+
if (dev.type in BOOL) return [BOOL[dev.type], dev.v?1:0];
78+
throw new Error(`Unknown device type ${E.toJS(dev.id)}`);
79+
}));
80+
return {
81+
0xFCD2 : adv
82+
};
83+
};

modules/BTHome.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<!--- Copyright (c) 2023 Gordon Williams, Pur3 Ltd. See the file LICENSE for copying permission. -->
2+
BTHome Library
3+
==============
4+
5+
<span style="color:red">:warning: **Please view the correctly rendered version of this page at https://www.espruino.com/BTHome. Links, lists, videos, search, and other features will not work correctly when viewed on GitHub** :warning:</span>
6+
7+
* KEYWORDS: Module,Modules,BTHome,Bluetooth,BLE,BT Home,Home Assistant,HomeAssistant
8+
* USES: BLE,Only BLE
9+
10+
[BTHome](https://bthome.io/) is an energy efficient but flexible BLE format for devices to broadcast their sensor data and button presses. It is supported by popular home automation platforms, like [Home Assistant](https://www.home-assistant.io/), out of the box.
11+
12+
13+
Usage
14+
-----
15+
16+
Call `require("BTHome").getAdvertisement()` with an array of things (`{type: string, v : value}`) to advertise, and it
17+
will return an advertisement that can be fed into `NRF.setAdvertising`:
18+
19+
```JS
20+
function updateAdvertising() {
21+
NRF.setAdvertising(require("BTHome").getAdvertisement([
22+
{
23+
type : "battery",
24+
v : E.getBattery()
25+
},
26+
{
27+
type : "temperature",
28+
v : E.getTemperature()
29+
}
30+
]), { name : "Sensor1" });
31+
}
32+
33+
// Update advertising now
34+
updateAdvertising();
35+
// Update advertising every 10 seconds...
36+
setInterval(updateAdvertising, 10000);
37+
```
38+
39+
See below for a list of allowable device types.
40+
41+
`getAdvertisement` adds a BTHome packet ID, which ensures that the data sent by the
42+
device will only be
43+
44+
**Note:** No size checking is performed, so if you advertise too much data,
45+
Espruino will start to remove characters from the device name to make room
46+
for the increased data length. As such it's best to try and keep your
47+
device name as short as possible.
48+
49+
50+
Device Types
51+
------------
52+
53+
54+
The allowed device types are (most of) what's mentioned in https://bthome.io/format
55+
and the names have been kept as similar as possible
56+
57+
### Values
58+
59+
60+
61+
* `battery` - 0..100, int
62+
* `temperature` - degrees C, floating point
63+
* `count` - 0..255, int
64+
* `count16` - 0..65535, int
65+
* `count32` - 0..0xFFFFFFFF, int
66+
* `current` - amps, floating point
67+
* `duration` - seconds, floating point
68+
* `humidity` - humidity %, int
69+
* `humidity16` - humidity %, floating point
70+
* `power` - power (W?), floating point
71+
* `pressure` - pressure (hPa), floating point
72+
* `voltage` - voltage (V), floating point
73+
* `text` - text string
74+
75+
### Events
76+
77+
* `button_event` - Call with `v` as one of: `none`,`press`,`double_press`,`triple_press`,`long_press`,`long_double_press` or `long_triple_press`
78+
* `dimmer_event` - Call this with am integer `v` value - 0 for no movement, negative for left or positive for right
79+
80+
As mentioned in the [BTHome docs](https://bthome.io/format), events are handled in Home Assistant 2023.5 and higher. You don't need to add a button/dimmer event if
81+
a button hasn't been pressed, however it may be easier to keep the field in, and if you want to have more than one button on a device then to get an event on the
82+
second button you need to define the first button with no event, for example:
83+
84+
```JS
85+
require("BTHome").getAdvertisement([
86+
{ type: "button_event", v: "none" },
87+
{ type: "button_event", v: "press" }
88+
]);
89+
```
90+
91+
Each time you update the advertising, the packet ID will increase, and another event will be sent to Home Assistant. As such we'd recommend that
92+
you make sure you clear the event flag after updating the advertisement. For example:
93+
94+
```JS
95+
var buttonState = false;
96+
97+
function updateAdvertising() {
98+
NRF.setAdvertising(require("BTHome").getAdvertisement([
99+
{
100+
type : "battery",
101+
v : E.getBattery()
102+
},
103+
{
104+
type : "temperature",
105+
v : E.getTemperature()
106+
},
107+
{
108+
type: "button_event",
109+
v: buttonState ? "press" : "none"
110+
},
111+
]), { name : "Sensor1" });
112+
// ensure that subsequent updates show button is not pressed
113+
buttonState = false;
114+
}
115+
116+
// Update advertising now
117+
updateAdvertising();
118+
// Update advertising every 10 seconds...
119+
setInterval(updateAdvertising, 10000);
120+
121+
// When a button is pressed, update advertising with the event
122+
setWatch(function() {
123+
buttonState = true;
124+
updateAdvertising();
125+
}, BTN, {edge:"rising", repeat:true})
126+
```
127+
128+
### Booleans
129+
130+
Just supply these with a single boolean value as v, for example: `{type:"door", v:true}`
131+
132+
* `battery_low`
133+
* `battery_charge`
134+
* `cold`
135+
* `connected`
136+
* `door`
137+
* `garage_door`
138+
* `boolean`
139+
* `heat`
140+
* `light`
141+
* `locked`
142+
* `motion`
143+
* `moving`
144+
* `occupancy`
145+
* `opening`
146+
* `power_on`
147+
* `presence`
148+
* `problem`
149+
* `tamper`
150+
151+
Notes
152+
------
153+
154+
At the moment, BTHome devices can only transmit data (not be connected to), so you can increase battery life
155+
of your device substantially by making it non-scannable and non-connectable. Note that you then won't be able
156+
to connect to reprogram it until you reset it!
157+
158+
The default advertising interval is 375m and you can also make that longer if you don't have updates to send
159+
very often.
160+
161+
You can also increase signal strength with `NRF.setTxPower(power)`.
162+
163+
```JS
164+
NRF.setAdvertising(require("BTHome").getAdvertisement([
165+
{
166+
type : "temperature",
167+
v : E.getTemperature()
168+
}
169+
]), { name : "Sensor1", scannable: false, connectable: false, interval: 600 });
170+
NRF.setTxPower(4); // NRF52840 devices like Bangle.js 2 can use 8!
171+
```
172+
173+
**Espruino will not normally advertise when you're connected to it,** but
174+
on firmwares 2v19 and later you can add `whenConnected:true` to the options at the
175+
end of the `NRF.setAdvertising` call to override that.
176+
177+
178+
References
179+
----------
180+
181+
There's an [app for Bangle.js](https://banglejs.com/apps/?q=bthome) which will make your [Bangle.js watch](https://www.espruino.com/Bangle.js2)
182+
advertise the current temperature and air pressure.
183+
184+
Mentioned on the forum in:
185+
186+
* [Integration with Home Assistant](https://forum.espruino.com/conversations/361380/)
187+
* [Home Assistant / EspHome/ BTHome / Bluetooth Proxies](https://forum.espruino.com/conversations/382301/)

0 commit comments

Comments
 (0)